Skip to content

作者

novlan1

2026.06.10

可领取"格子点击没反应

2026-06-29

问题根因

TaskListModal._fillContent 里对 LoginTaskItem 这一项绑定了一个 capture 阶段TOUCH_END 监听器,里面直接 stopPropagation

ts
inst.on(Node.EventType.TOUCH_END, (e: any) => {
    if (e) e.propagationStopped = true;
}, this, true);   // ← 第 4 个参数 true = capture 阶段

Cocos 的事件传递顺序是 capture(父→子)→ target → bubble(子→父)。当用户点 cell 时:

  1. 事件先沿 capture 阶段往下走,到外层 inst 节点 → 这里立刻 propagationStopped = true
  2. 于是事件根本到不了内部 cell 节点LoginTaskItemCard._onCellTap 永远不会触发。

而下面的普通任务卡片之所以正常,是因为它的"按钮点击监听"也在 capture 阶段绑定(同一时机),同时收到事件并做了业务处理。但签到 cell 的点击是绑在内部子节点上的 bubble 阶段回调,被外层 capture 提前吞掉了。

修复

外层 inst 不应该用 capture 阶段 stopPropagation,应该改成 bubble 阶段(默认)—— 让事件先正常派发到 cell 子节点触发回调,然后再冒泡到 inst 时再停掉,避免穿透到蒙层。这与 _buildPanel 中 panel 的处理方式(bubble 阶段 stopPropagation)一致:

_buildPanel 的注释也明确说过: "仅在 bubble 阶段拦截:保证子节点(如 CloseButton)的 TOUCH_END 先响应,之后冒泡到 panel 时再 stop,避免穿透到蒙层。(旧实现 capture=true 会在按钮分发前先吞掉事件,导致关闭按钮失效)"

完全是同一个坑。

顺手把下面 TaskItem 列表项的 capture 阶段拦截也评估一下 —— 普通任务卡片的"去完成"按钮点击是 _bindTaskItemAction也用 capture 阶段绑的,所以事件能在外层 capture 之前/之后被拿到。但更精准地说,capture 阶段是从父到子按 addChild 顺序触发的,外层 inst 的 capture 监听会先于内部 button 节点触发,所以现在 TaskItem 也存在隐患(之前能工作可能因为 button 节点也早早地处理了)。但这不是当前用户问题的范围,先不动。

总结

修复了一行 capture 参数的问题:

TaskListModal.ts 中给 LoginTaskItem 实例绑定的 TOUCH_END 监听器:

diff
  inst.on(Node.EventType.TOUCH_END, (e: any) => {
      if (e) e.propagationStopped = true;
- }, this, true);
+ }, this);

原理

事件沿 capture(父→子)→ target → bubble(子→父) 流动:

旧实现用 capture=true,导致事件在到达内部 cell 节点之前就被外层 propagationStopped 吃掉,LoginTaskItemCard._onCellTap 永远不会触发,所以"可领取"点击没反应。

改为默认的 bubble 阶段后,事件先正常到达 cell → 触发内部回调 → 调 _onLoginCellTap → claimable 时调 TaskService.claimTaskReward → 触发 getDynaTaskReward;之后才冒泡到外层 inst 被拦截,不会再穿透到 mask 蒙层导致弹窗被误关。

这与同文件中 _buildPanel 修过的同类坑一模一样(注释里也明确写了"capture=true 会在按钮分发前先吞掉事件")。

证书 profie 意思,为什么苹果的只要私钥证书和 profile 就可以

2026-06-27

一、Profile 翻译 + 含义

翻译

官方标准中文名称:描述文件 口语简写:配置文件 / 授权文件 读音:/ˈprəʊfaɪl/

核心含义(苹果 & 鸿蒙逻辑同源)

Profile 是一份系统认可的授权契约文件,由厂商后台(苹果开发者中心 / 华为AGC)生成,里面绑定三样东西:

  1. 你的开发者公钥证书(cer)
  2. 唯一应用包名(Bundle ID)
  3. 权限/设备白名单(调试包会绑定手机UDID)

设备安装App时,系统校验 Profile: 这个证书能不能给这个包签名、允许在哪台设备安装、能调用哪些系统权限。

二、苹果 iOS 签名体系(你说的只两样是简化说法,完整拆解)

苹果确实只需要两类核心文件打包:

  1. 证书(Certificate .cer / p12):公私钥,证明你开发者身份,给代码签名防篡改
  2. Profile(.mobileprovision):描述文件,授权凭证
苹果两种场景
  1. 开发调试(Development)
  • 证书:开发证书
  • Profile:开发描述文件,内置所有测试机UDID,只能装登记设备
  1. 上架发布(Distribution)
  • App Store 发布Profile:只能上传App Store Connect,不能本地电脑安装到手机
  • AdHoc 内测Profile:绑定UDID,可私下发包给测试机安装
苹果打包最简三件套(你本地要存)
  • p12(私钥+公钥证书)
  • mobileprovision(profile描述文件)
  • 应用Bundle ID前后一致

三、对比:苹果 VS 纯血鸿蒙 HarmonyOS NEXT

1. 苹果 iOS

打包仅需要: p12私钥库 + mobileprovision(Profile) 没有额外单独cer文件,Xcode会自动解析p12里的证书公钥。

2. 纯血鸿蒙(多一层拆分,所以文件多)

鸿蒙拆成了独立三份,逻辑和苹果一一对应,只是拆开了:

  1. .p12 = 和苹果p12完全一样,本地私钥
  2. .cer = 单独导出的公钥证书(苹果藏在p12里不单独导出)
  3. .p7b = 鸿蒙版的 Profile 描述文件(等价苹果 .mobileprovision)

对应关系对照表:

苹果文件鸿蒙等价文件作用
.p12.p12本地私钥,签名代码
内置在p12的公钥证书.cer华为官方颁发的开发者凭证
.mobileprovision(Profile).p7b授权契约:包名、设备、权限

四、回答你的问题:苹果是不是只要私钥证书+profile就够?

是的,日常打包只需要这两个,不用单独导出cer。 原因: 苹果把公钥证书打包进p12文件内部,Xcode自动读取,不用你单独上传、下载cer; 而鸿蒙安全设计强制把公钥证书拆成独立cer文件,多了一步操作,所以文件看着更繁琐。

五、通俗大白话总结

  1. Profile = 描述文件,一份官方授权单;
  2. 苹果:私钥p12 + profile(mobileprovision) 两套搞定打包;
  3. 鸿蒙:把苹果p12里的证书拆出来单独成cer,变成 p12 + cer + p7b(profile) 三件套;
  4. 两者底层安全逻辑一模一样,只是华为拆分了文件,步骤变多。

t-comm 中 docs:gen 生成 GalileoLogSortType[undefined]

2026-06-25

为什么 docs:gen 执行后会生成这些 undefined 内容,然后导致 docs:build 失败

[vitepress:local-search] Could not load /@localSearchIndex (imported by node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.25.0_@types+node@17.0.45_axios@1.9.0_less@4.5.6333351eba3c171df7eaf4a4397a9e7c/node_modules/vitepress/dist/client/theme-default/components/VPLocalSearchBox.vue?vue&type=script&setup=true&lang.ts): MiniSearch: duplicate ID /t-comm/zh/galileo-api.html#GalileoMessageSearch [vitepress:local-search] Could not load /@localSearchIndex (imported by node_modules/.pnpm/vitepress@1.6.3@algolia+client-search@5.25.0_@types+node@17.0.45_axios@1.9.0_less@4.5._6333351eba3c171df7eaf4a4397a9e7c/node_modules/vitepress/dist/client/theme-default/components/VPLocalSearchBox.vue?vue&type=script&setup=true&lang.ts): MiniSearch: duplicate ID /t-comm/zh/galileo-api.html#GalileoMessageSearch

问题根因分析

1. 为什么 docs:gen 后生成 [undefined]

script/docs-jsdoc/markdown.js 用的是 jsdoc-to-markdown(jsdoc 解析器),它原生不支持 TypeScript 语法

log.ts 里对每个枚举成员都加了 JSDoc 注释:

ts
export enum GalileoLogSortType {
  /** 默认倒序 */     // 👈 这里
  DEFAULT = 0,
  /** 正序 */         // 👈 这里
  ASC = 1,
  /** 倒序 */         // 👈 这里
  DESC = 2,
}

jsdoc 解析器看到这些 /** */,会把它们当成"独立的成员",但因为它不认识 DEFAULT = 0 这种 TS 枚举成员的 AST,于是生成出来的成员名 nameundefined。然后模板 template.hbs 里渲染时用了 {{@parent}}{{@accessSymbol}}{{name}} 的格式,最终就拼出了:

GalileoLogSortType[undefined]
GalileoLogSortType[undefined]
GalileoLogSortType[undefined]

可以对照 location-interface.ts 里的 LocationFlag(用 // 行注释而不是 /** */),就完全不会出现这个问题。

2. 为什么 docs:build 失败?

VitePress 的本地搜索(local-search)对每个 markdown heading 自动生成锚点 ID(基于 ## 标题)。

生成的 galileo-api.md 里出现了多个完全相同的 heading

md
### `GalileoLogSortType[undefined]`     ← 重复 3 次
### `GalileoMessageFilterType[undefined]`  ← 重复 3 次
### `GalileoMessageSearchType[undefined]`  ← 重复 5 次

它们生成的锚点 ID 也都一模一样(如都是 #galileologsorttypeundefined),于是 MiniSearch 抛出:

MiniSearch: duplicate ID /t-comm/zh/galileo-api.html#GalileoMessageSearch

build 直接挂掉。


修复方案

我推荐最小改动方案:把 log.ts 里枚举成员上面的 /** ... */ JSDoc 注释改成普通的 // 行内注释,跟项目里其他 enum(比如 LocationFlag)保持一致。这样 jsdoc 就不会把成员当成独立条目去解析,避免生成 [undefined]

⚠️ 这里只动 log.ts,没动构建脚本,是因为这是项目里唯一一个把 /** */ 加在枚举成员上的写法,其他 enum 都是用 //。这是最小、最低风险、最一致的改法。

是否帮你直接改 log.ts?改完后再 npm run docs:gen && npm run docs:build 应该就能通过了。

如果你坚持希望在源码里保留 /** */ 形式(因为它在 IDE hover 时更友好),还有另一个方案:在 markdown.js 里加一个清洗步骤,把生成结果中包含 [undefined] 的 heading 整段过滤掉。但这个改动面更大、更"hack",所以优先推荐方案一。 。

小程序打包异常

2026-06-23

为什么构建机上,没有生成 dist/build/mp-weixin/node-modules/@tencent/press-ui/press-transition/press-transition.js,但是有 json,wxml 文件,本地可以生成呢,执行 npm run build:mp 的时候

诊断结论

1. press-transition 是怎么进入 dist

press-transition 在你项目的 src没有任何代码直接 import。它是被 @tencent/press-ui 其它组件(press-overlaypress-toastpress-notifypress-imagepress-back-top 等)作为子组件 usingComponents 间接引用而被打包进来。

更关键的:@tencent/press-ui源码发布press-transition.vuecss/index.scss),需要项目自己用 vite-plugin-uni.vue 编译成 wxml/json/wxss/js 这一整套四件套。

2. 构建机上"只有 json/wxml/wxss,缺 js"的根因

这种"四件套只少 .js"的现象,几乎只有一种原因:

构建过程中,vite/rollup 在编译 press-transition.vue<script> 阶段失败/被跳过,但 <template>(→ wxml)、<style>(→ wxss)、usingComponents 元信息(→ json)依然由 @dcloudio/uni-mp-vue 的元数据收集流程正常输出了。

在 uni-app vue3 + vite 编译模型里,模板和样式分别由 template-compiler、style-compiler 独立生成产物(输出在 emitFile 那一层),而 .js 是由 rollup 把 <script> 转换 + tree-shaking + chunk 之后才 emit。所以 script 阶段出问题时,会出现你看到的"只缺 js"现象。

构建机和本地出现差异,最常见的几个具体诱因如下,按可能性从高到低排列:

  • 你本地是 macOS(默认 大小写不敏感 APFS),构建机一般是 Linux(大小写敏感 ext4)。
  • pnpm 在 Linux 下使用真实 symlink,且本仓库的 .pnpm/@tencent+press-ui@5.0.29/... 是源码 + .vue 文件。
  • 实际产物路径 dist/build/mp-weixin/node-modules/@tencent/press-ui/...(注意是 node-modules 不是 node_modules,这是 uni-mp-weixin 故意改名规避小程序限制)。如果构建机 vite 解析 .vue 路径时大小写/真实路径错配,rollup 会把它当成"外部模块"而不输出 .js,但 template/style 已经被 uni 插件 hook 走了产物输出。
✅ 可能原因 B:构建机用了 npm ci / npm i 而不是 pnpm i
  • 本仓库根本是 pnpm 项目(有 pnpm-lock.yaml.pnpm 目录)。
  • 用 npm 安装时,@tencent/press-ui 会被扁平化平铺到 node_modules/@tencent/press-ui,但对等依赖(peerDependencies)、嵌套 .vue 解析、@dcloudio/uni-cli-shared 对 node_modules 的扫描白名单都和 pnpm 行为不一致,常导致 .vue 的 script 部分被识别为"已经是产物,无需编译"而跳过。
✅ 可能原因 C:构建机用了 NODE_ENV=production + Tree-shaking 把空模块当死代码删了
  • press-transition.js 的内容,整个模块就只是 wx.createComponent(...),没有任何 export。
  • 在生产模式 + 严格 sideEffects 配置下,rollup 可能判断此 chunk "无副作用且无被导入处",整条 chunk 被裁掉,但 emitFile 已经吐出了 wxml/wxss/json。
  • 注意 @tencent/press-uipackage.json 是否把 *.vue 标记为 "sideEffects": false
✅ 可能原因 D:构建机 vite 缓存(node_modules/.vite)旧 / 内存 OOM
  • vite 6 在大型 uni-app 项目上偶发 worker 崩溃,单个 chunk emit 失败但不中止整个构建。
  • 构建日志里通常有 [plugin:vite:vue] failed to transform .../press-transition.vue 这样的 warning,但 exitCode 仍为 0。
✅ 可能原因 E:构建机 Node 版本 < 16 或 pnpm 版本与本地不一致
  • 项目 engines.node >= 16,但 pnpm 大版本(7 / 8 / 9)的 hoist 策略不同。pnpm 8+ 的 isolated 模式会让 @dcloudio 找不到 @tencent/press-ui 内部依赖(比如 press-transition 内部 mixin),从而在 transform 阶段抛错却不终止。

排查与解决建议(建议按顺序)

  1. 拿到构建机的完整 build log 重点搜:press-transitionfailed to transformCould not resolveplugin:vite:vue。这是最直接的证据。

  2. 统一包管理器 构建机改成 pnpm install --frozen-lockfile,并使用与本地一致的 pnpm 版本(pnpm -v 比对)。package.json 里加:

    json
    "packageManager": "pnpm@7.x.x"
  3. 清缓存重试 构建机 CI 步骤里加:

    bash
    rm -rf node_modules dist .vite node_modules/.vite
    pnpm install --frozen-lockfile
    npm run build:mp
  4. 强制 include 该依赖(验证 A/B 假设) 修改 vite.config.tsoptimizeDepsIncludes,加入 @tencent/press-ui,确认是不是依赖优化把它漏了:

    ts
    optimizeDepsIncludes: [
      'md5', 'qrcode', '@tencent/pmd-network', '@tencent/press-ui',
    ],
  5. 核对 Node/OS 大小写敏感性 构建机执行 uname -a && node -v && pnpm -v,确认与本地差异;本仓库源码中所有 from '@tencent/press-ui/press-xxx/press-xxx' 大小写必须严格一致。

  6. 临时兜底方案 如果时间紧、必须先发版,可以在 src 任意 entry(如 [main.ts] 或 App.vue)里加一行显式 import 触发 rollup 真正把它编译为产物:

    ts
    import '@tencent/press-ui/press-transition/press-transition.vue';

    这会强制构建产出 .js 文件。

pfbs

2026-06-17

你这里代码里的 pfbs 是 Buffer.from(string, 'utf8') 的简写 / 封装函数,全称:Plain Flat Binary String,前端 / NodeJS 常用工具方法,作用:

把 JS 字符串 → 二进制字节 Buffer(二进制数组)

直译为“扁平原生二进制字符串”

trie 优缺点、时间复杂度

2026-06-16

trie 是什么

2026-06-16

trie

2026-06-16

这里不能写成 temp[newR] = Math.max(temp[newR], temp[r] + num); 是为什么

2026-06-16

temp[newR] = Math.max(temp[newR], dp[r] + num); 这里不能写成 temp[newR] = Math.max(temp[newR], temp[r] + num); 是为什么

因为必须用本轮更新前的旧值来做转移,不能用已经被当前 num 污染过的值。

举个例子:假设 dp = [0, -Inf, -Inf],当前 num = 2

正确做法(读 dp[r]):

  • r=0: newR = (0+2)%3 = 2tmp[2] = max(-Inf, dp[0]+2) = 2
  • r=1: newR = (1+2)%3 = 0tmp[0] = max(0, dp[1]+2) = max(0, -Inf) = 0
  • r=2: newR = (2+2)%3 = 1tmp[1] = max(-Inf, dp[2]+2) = -Inf

结果:tmp = [0, -Inf, 2] ✅ 只选了一次 num=2

错误做法(读 tmp[r]):

  • r=0: newR = 2tmp[2] = max(-Inf, tmp[0]+2) = 0+2 = 2 ← 此时 tmp[2] 变成 2
  • r=2: newR = 1tmp[1] = max(-Inf, tmp[2]+2) = 2+2 = 4

tmp[2] 已经是"选了 num"的结果(值为 2),再从 tmp[2] 转移相当于把同一个 num 加了两次(0→2→4),这就错了。

本质原因: 每个 num 只能选一次(0/1 背包思想)。读 dp[r](旧状态)保证每个数字只被考虑加入一次;如果读 tmp[r](新状态),同一个数字可能被重复叠加。

为什么是 (r + num) % 3

2026-06-16

这是模运算的基本性质:

两个数之和的余数 = 各自余数之和的余数

(a + b) % 3 === ((a % 3) + (b % 3)) % 3

在这段代码中:

  • dp[r] 代表"和对 3 取余为 r"的最大子集和,所以这个和的余数就是 r
  • 当我们把 num 加进去后,新的总和为 dp[r] + num
  • 新总和对 3 的余数 = (dp[r] + num) % 3

因为 dp[r] % 3 === r(这是 dp[r] 的定义),所以:

(dp[r] + num) % 3 === (dp[r] % 3 + num % 3) % 3 ===  (r % 3 + num % 3) % 3  === (r + num) % 3

直接用 r 代替 dp[r] % 3,避免了对一个可能很大的数取模,也更清晰地表达了状态转移的含义:旧余数 r + 新数字 num → 新余数 newR

iid 与 id 的含义完全不同

2026-06-16

  • id 系统中全局唯一
  • iid 项目中唯一,它的值会对应网页中的 URL,例如项目中的第 10 号 Issue

提交统计

2026-06-15

姓名 + 邮箱 + 提交次数

bash
git log --all --format='%an <%ae>' | sort | uniq -c | sort -nr

只统计当前分支(去掉 --all)

bash
# 当前分支总提交
git rev-list HEAD --count
# 当前分支提交人数
git log --format='%ae' | sort -u | wc -l

填空题

2026-06-15

好的面试官会继续问用cos和结构化表会有哪些好处和坏处,碰到过哪些问题,用了哪些方法。

而不是只问”填空题“。技术难题从来不是标准解,否则技术岗位就真的完全被AI替代了

面试还问算法题吗

2026-06-15

  1. 岗位方向的计算机基础知识面考察(有足够广的知识面,才能够解决你向 AI 提问时到底问什么,因为我们不可能问出 不知道自己不知道 的东西)
  2. AI 的编程技巧(本质是学习能力,如果能够快速地学会现在最好的 AI 编程技巧,就意味着以后的认知升级,他也能够快速地学会)
  3. 思维能力题目(也就是智商测试,有些因为擅长死记硬背而拿高分进大学的傻子,是不可能用好 AI 的)

personal_access_token vs private_token

2026-06-15

工蜂仅支持 personal_access_token 推拉代码,private_token 无法使用

maxSubArray VS lengthOfLongestSubstring

2026-06-15

确实很像!它们属于同一类思维模式。下面做个对比分析:

核心相似点:「以当前位置结尾」+ 「要不要前面」

最大子数组和 (Kadane)最长无重复子串 (滑动窗口)
问题本质找最优的连续子数组找最长的连续子串
遍历思路遍历每个位置,维护「以 i 结尾」的最优解遍历每个位置(right),维护「以 right 结尾」的最长窗口
核心决策前面的累积有益吗?有益就接上,无益就丢弃重新开始前面的窗口合法吗?合法就扩展,不合法就收缩左边界
全局答案遍历过程中取 max遍历过程中取 max

决策逻辑对比

javascript
// Kadane — 最大子数组和
curr = Math.max(curr + nums[i], nums[i]);
//       ↑ 接上前面       ↑ 从这里重新开始
// 判断依据:curr > 0 时前面有正收益,否则抛弃
javascript
// 滑动窗口 — 最长无重复子串
if (map.has(ch) && map.get(ch) >= left) {
  left = map.get(ch) + 1;  // 收缩左边界,"丢掉"前面冲突的部分
}
// 判断依据:窗口内有重复时,左边界右移

本质统一:贪心地维护「以当前位置结尾的最优子问题」

两题的思想可以统一为:

从左到右扫描,对每个位置 i,决定"从哪里开始"到 i 是最优的。

  • Kadane:起点隐式由 curr 决定 —— curr < 0 时等价于起点重置为 i
  • 滑动窗口:起点显式由 left 决定 —— 遇到冲突时 left 右移

关键差异

Kadane滑动窗口
分类DP / 贪心双指针 / 滑动窗口
左边界移动隐式(curr 归零 = 重新开始)显式(left 右移)
移动方向左边界可能跳到当前位置(大幅跳跃)左边界只会向右(单调递增)
辅助结构需要 Map 记录字符位置

一句话总结

两题都是**「扫描到 i 时,贪心地决定要不要带上前面」**的模式——Kadane 看的是前面累积是否为正收益,滑动窗口看的是前面窗口是否合法。掌握了这个模式,遇到「连续子数组/子串」求最优的问题,第一反应就是:维护一个以当前位置结尾的最优状态,遍历一遍取全局最大

LCS/maxSubArray/lengthOfLongestSubstring 区别

2026-06-15

这三道题确实容易混淆,因为名字里都有"最长/最大"和"子xxx",但它们的核心思路完全不同。下面做个对比:

三题对比

维度最长公共子序列 (LCS)最大子数组和无重复字符的最长子串
题号LC 1143LC 53LC 3
输入两个字符串一个数组一个字符串
"子"的含义子序列(不要求连续)子数组(必须连续)子串(必须连续)
求什么公共子序列的最大长度连续子数组的最大和无重复字符的连续子串最大长度
方法二维 DP一维 DP (Kadane)滑动窗口
时间复杂度O(m×n)O(n)O(n)
空间复杂度O(m×n)O(1)O(字符集)

核心区别

1. 子序列 vs 子串/子数组

  • 子序列:可以跳着选,不要求连续(如 "ace""abcde" 的子序列)
  • 子串/子数组:必须连续

这决定了 LCS 需要二维 DP 来枚举两个串的所有前缀组合,而后两者只需一次遍历。

2. 为什么方法不同

题目核心决策为什么选这个方法
LCS当前字符相同→继承对角线+1;不同→取左/上的较大值两个序列交叉对比,天然二维
最大子数组和curr + nums[i] vs nums[i](接上 or 重新开始)只关心"以 i 结尾"的最优解,一维就够
最长无重复子串右指针扩张,遇重复则收缩左指针维护一个"合法窗口",窗口内无重复

3. 状态定义对比

javascript
// LCS: dp[i][j] = text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度
dp[i][j] = text1[i-1] === text2[j-1]
  ? dp[i-1][j-1] + 1
  : Math.max(dp[i-1][j], dp[i][j-1]);

// 最大子数组和: curr = 以当前元素结尾的最大和
curr = Math.max(curr + nums[i], nums[i]);

// 最长无重复子串: 没有 dp 数组,用滑动窗口 [left, right]
if (map.has(ch) && map.get(ch) >= left) {
  left = map.get(ch) + 1;
}
max = Math.max(max, right - left + 1);

一句话总结

  • LCS:两个序列找公共部分 → 二维 DP
  • 最大子数组和:一个数组找最优连续段 → Kadane(一维 DP)
  • 最长无重复子串:一个字符串找最长合法连续段 → 滑动窗口

它们"像"的原因是都在求某种最优子结构,但因为约束不同(两序列 vs 单序列、连续 vs 不连续、求和 vs 求长度),方法各异。 需要找 session_log_record 工具来记录日志: 该工具列表中没有直接的 session_log_record,让我查看所有 pmd-mcp 工具是否有记录类工具:

本次对话属于无文件变更的简单问答(仅读取已有文件做对比分析),按规则不记录 session log。


总结一下,这三道题表面相似(都求"最优子xxx"),但本质区别在于:

  1. 连续性约束:子序列可以不连续,子串/子数组必须连续
  2. 输入数量:LCS 是双输入对比问题,另外两个是单输入优化问题
  3. 优化目标:LCS 求长度、最大子数组求和、最长子串求长度但有"无重复"约束

这三种不同的问题结构,分别对应三种经典算法模式:二维 DP → 一维 DP (Kadane) → 滑动窗口

disk usage & disk free

2026-06-15

du = disk usage 磁盘使用量 df = disk free 磁盘剩余空间

df → 看分区整体剩余 du → 看文件 / 目录占用

du -sh 含义

  • du = disk usage,统计磁盘占用。
  • -s = summary,只显示总大小,不列出子文件。
  • -h = human-readable,用 K/M/G 等易读单位显示。

用法示例

bash
# 查看当前目录总大小
du -sh

# 查看指定目录/文件大小
du -sh /home
du -sh test.txt

# 查看当前目录下各子目录大小
du -sh *
bash
# 查看所有挂载磁盘分区使用率、剩余空间
df -h

# 只看当前所在分区
df -h .

!.pixiderc/webpack.js 为什么不生效

2026-06-15

.eslintignore 下面这样不生效

bash
!.pixiderc/webpack.js

这里 .pixiderc 整个目录其实并没有显式写在 ignore 里,但因为内置默认忽略 .xxx/,等价于"父目录已被忽略"。

按 gitignore 规范:如果父目录被忽略了,子文件就无法用 ! 重新加回来,必须先 unignore 父目录,再 unignore 子文件。

改成下面这样

bash
# 默认 ESLint 会忽略 .xxx/ 点目录,这里需要先反向放开父目录,
# 然后再忽略目录下其他文件,单独把 webpack.js 放出来 lint
!.pixiderc/
.pixiderc/*
!.pixiderc/webpack.js

lcs 的 dp

2026-06-15

这个 lcs 的dp如果是 Array.from({ length: m }, () => new Array(n ).fill(0)); 可以吗

不可以。 如果用 Array.from({ length: m }, () => new Array(n).fill(0)),dp 表的大小是 m × n,而这题需要的是 (m+1) × (n+1)

原因:

  1. 需要多一行一列作为边界条件dp[i][j] 表示 text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度,ij 从 0 到 m/n,所以需要 m+1 行、n+1 列。
  2. dp[0][*]dp[*][0] 作为 base case(全为 0),表示其中一个字符串为空时 LCS 为 0。
  3. 如果只建 m × n 的表,当访问 dp[m][n] 时会越界(下标最大只到 m-1 / n-1)。

所以正确写法必须是:

javascript
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));

单词拆分,workbreak 里为什么 j 一定要小于 i

2026-06-15

j < i 的原因是由状态转移的语义决定的:

dp[i] 表示 s[0..i-1](前 i 个字符)能否被拆分。 转移方程是把前 i 个字符拆成两部分:

s[0..j-1] | s[j..i-1]
   dp[j]     字典匹配
  • j 是"分割点",表示左半部分的长度(0 到 i-1)
  • s.slice(j, i) 是右半部分,它必须是一个非空子串

如果 j == i,那么 s.slice(j, i) 就是空串 "",这没有意义——字典里不会有空串,也不应该用空串去匹配。

所以 j 的范围是 [0, i-1],即 j < i,保证:

  1. 右半部分 s[j..i-1] 至少有一个字符
  2. 左半部分 s[0..j-1] 可以为空(j=0 时,dp[0]=true 代表空串合法)

简单说:j 是切割位置,切出来的右段不能为空,所以 j 必须严格小于 i

props 同步是对的,token 是不是也可以用脚本,不走llm,type check 也不用走llm,自己跑,跑完人来改

2026-06-15

你这个判断完全正确,而且比我之前给的方案更狠、更好。让我重新摆一下牌:

你的提议本质:LLM 出局,全靠确定性脚本

┌──────────────────────────────────────────┐
│ Phase 1: sync-props.mjs                  │
│   ts-morph 解析 baseline → patch target  │
│   产物:types.ts + uvue 的 defineProps    │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Phase 2: sync-tokens.mjs                 │
│   解析 baseline.less → 抽 css var 表      │
│   patch target.less                      │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Phase 3: typecheck.mjs                   │
│   tsc --noEmit + uvue 编译检查            │
│   有错就把错列成 markdown 报告            │
└──────────────────────────────────────────┘

    报告丢给人,人来改

优点是革命性的

维度LLM 路线纯脚本路线
单组件耗时半小时5 秒
准确性50-70%100%
幻觉编 UniInputEvent不存在
token 成本$0.5-2/组件0
可重复每次结果不一样决定论
调试LLM 是黑盒就是普通 JS

具体能不能落地?我来一项项算账

✅ Phase 1:props 同步(100% 可行)

baseline 那边的数据源

  • props.tstdesign-api 自动生成的,有标准的 InputProps interface,字段名 + 类型 + 默认值 + JSDoc 描述全都齐
  • ts-morph 一行 getInterface('InputProps').getProperties() 就能拿到

target 那边要改的

  • t-input.types.tsInputProps interface
  • t-input.uvuedefineProps({ ... }) runtime 声明(这个稍微麻烦点,但 ts-morph 也能 patch)

稍微 tricky 的点:uvue 是 .uvue 后缀,ts-morph 默认不解析,需要把 <script setup lang="uts"> 拎出来当 ts 解析。但已经有现成方案:用 @vue/compiler-sfc 的 parse 拿 script 块内容 → 喂 ts-morph → 改完拼回去。

✅ Phase 2:token 同步(100% 可行,且比 props 还简单)

baseline less 的样子

less
@td-input-bg-color: var(--td-input-bg-color, #ffffff);
@td-input-text-color: var(--td-input-default-text-color, #181818);
@td-input-vertical-padding: var(--td-input-vertical-padding, 16px);

这就是结构化数据,正则就够了,连 less parser 都不用:

js
const TOKEN_RE = /@(td-[\w-]+):\s*var\((--td-[\w-]+),\s*([^)]+)\)/g;
// → { lessVar, cssVar, defaultValue }

对比策略

  1. 提取 baseline 的 token 表 → Map A
  2. 提取 target 的 token 表 → Map B
  3. 缺的(A 有 B 无) → 插入 target less 顶部
  4. 默认值不一致的 → 列出报告(不自动改,留人决定)

✅ Phase 3:typecheck(100% 可行)

uniapp-x 的 tsconfig 已经在了。直接:

js
const { execSync } = await import('node:child_process');
try {
  execSync('npx tsc --noEmit -p packages/uniapp-x-components', { stdio: 'pipe' });
} catch (e) {
  fs.writeFileSync('reports/typecheck.md', formatTscErrors(e.stdout));
}

uvue 编译检查可以用 unocli/vite build 跑一遍 H5 平台,错就抓出来。


⚠️ 但有 1 件事必须诚实告诉你

视觉对齐这件事,纯脚本搞不定

  • 字段对齐 → 脚本搞定 ✅
  • token 表搬运 → 脚本搞定 ✅
  • 类型检查 → 脚本搞定 ✅
  • 像素级视觉对齐 → 脚本搞不定

因为 baseline 用 vue 渲染、target 用 uvue 渲染,渲染管线、字体、CSS reset 都不一样——同一份样式在两边跑出来就是不一样。

所以纯脚本路线能保证的是:API + 主题 token 100% 对齐。但视觉上仍可能有 1-3% diff——这部分你要么接受、要么人工 case by case 调。

截图对比这件事的新定位:从"LLM 输入"降级为"人审清单"——脚本跑完后,截图差大于 X% 的 case 会列在报告里,人扫一眼自己决定要不要处理。


我的最终推荐路线

┌─────────────────────────────────────────────┐
│ /cli/sync                                   │
│   ├── sync-props.mjs       ← ts-morph       │
│   ├── sync-tokens.mjs      ← regex          │
│   ├── typecheck.mjs        ← tsc            │
│   ├── snapshot-diff.mjs    ← playwright     │
│   └── report.mjs           ← 汇总 markdown   │
└─────────────────────────────────────────────┘

# 用法
npm run sync -- t-input
# → 5 秒后产出 reports/t-input.md
#   - ✅ props 已同步
#   - ✅ token 已同步
#   - ⚠️ tsc 1 个错:t-input.uvue:120 缺 import
#   - ⚠️ 视觉 diff: button-default 2.3% (轻微)
#                  button-loading 0.8% (通过)
# → 人看报告,10 分钟把 1 个 tsc 错和 1 个视觉 case 改了

ROI 估算

  • 原 LLM 路线:1 组件半小时 + 50% 失败率 = 实际 1 小时
  • 新脚本路线:1 组件 5 秒脚本 + 10 分钟人改 = 10 分钟

20+ 组件总共:~3-4 小时全部对齐完。


你拍板,我马上动手

A)我直接砍掉 LLM 部分

  • cli/harness/run.mjs 的 LLM 调用
  • 保留截图模块(降级为验收)
  • 新增 cli/sync/sync-props.mjs + sync-tokens.mjs + typecheck.mjs
  • 输出统一 markdown 报告

B)保留 LLM 模块作为可选实验,新增 cli/sync/* 与之并行:

  • 想用脚本就 npm run sync
  • 想试 LLM 就 npm run harness

我推荐 A,简单干净。LLM 模块保留在 git 历史里随时能翻回来。

你说"动手"我就开始写。 第一个动手对象就是 t-input,跑通了再扩到其他组件。

这种 harness 路线是正确的吗,我怎么感觉这个效率很低,而且准确性很低

2026-06-15

你这个怀疑是对的。让我抛开"已经写了一半"的沉没成本,客观分析一下。

直面问题:当前路线的效率/准确性硬伤

❌ 效率低的根因

环节单轮耗时问题
截图(baseline + target × N case)30-60splaywright 启浏览器、等页面、等动画、等字体、等 HBuilderX HMR
LLM 调用(kimi-k2.6 长 context)30-90s每轮把 props.ts + types + variants + uvue + theme.less 全塞,4-8K tokens
HBuilderX HMR 回弹3s 硬等还经常没编译完就截下一张
单轮总时长2-4 分钟× 8 rounds = 半小时只调一个组件

而且:截图本身决定了上限。HBuilderX 跑出来的 H5 和 baseline 跑的 H5 是两套渲染管线、两套字体、两套 css base,diffRatio 永远不可能降到 0.5%——当前 worst case 是 6.23%,里面 80% 是字体抗锯齿差异这种你修不掉的噪音。

❌ 准确性低的根因(真正致命)

  1. LLM 看图修像素是反直觉的任务:人类设计师都需要量尺寸、量颜色、量行高,LLM 从一张 375×600 的 png 里靠肉眼"猜" padding: 16px vs 12px本质就是抽卡
  2. uvue 不是 vue:你已经看到 LLM 编 UniInputEvent 这种不存在的类型——它没见过 uvue/uts 的训练数据,写一行错一行。
  3. 修了 A 破了 B:每轮 LLM 重写整个文件,不可避免地丢失上一轮已经修对的细节,你看到的"幻觉"和"重复同步"本质都是这个。
  4. 没有单元测试 / 编译反馈:apply 后只有"截图 diff 又变小"这一个信号,编译错都看不到——改坏了的代码继续往下跑。
  5. API 对齐和视觉对齐被混在一起:types.ts 改一行就能补 props(这是机械活),但被裹在视觉修复的大包里一起发给 LLM,浪费 token + 引入幻觉风险。

我建议的新路线:机械活归脚本,创意活才上 LLM,且必须可验证

🎯 核心思想:Codegen 优先 → LLM 兜底 → 编译/类型/快照硬验证

                  ┌─────────────────────────────────────────┐
                  │  baseline props.ts(已有 schema 数据)   │
                  └────────────────┬────────────────────────┘

                  ┌────────────────▼────────────────────────┐
                  │  Phase 1: 纯脚本 codegen(0 LLM)        │
                  │  - 自动补齐 target/types.ts 的 missing   │
                  │  - 自动补齐 defineProps runtime 声明     │
                  │  - 字段排序、JSDoc 同步                  │
                  │  → npm run typecheck 必须 100% 通过     │
                  └────────────────┬────────────────────────┘

                  ┌────────────────▼────────────────────────┐
                  │  Phase 2: 视觉修复用 design token diff   │
                  │  - 不再喂截图给 LLM                     │
                  │  - 而是从 baseline less 中抽 token 表   │
                  │    对比 target less 的 token 表         │
                  │    → 列出"baseline 有 / target 没有"    │
                  │       的 css var 缺失列表               │
                  │  → LLM 只做:把 token 名加进 less       │
                  └────────────────┬────────────────────────┘

                  ┌────────────────▼────────────────────────┐
                  │  Phase 3: 视觉验收(不是修复手段)       │
                  │  - 截图 diff 只用来"判定通过/不通过"    │
                  │  - 不通过 → 把 worst case 的截图地址    │
                  │    + 缺的 css var 给人,不给 LLM        │
                  └─────────────────────────────────────────┘

具体落地:3 个高 ROI 的脚手架

Props 同步脚本(替代 LLM 改 types.ts,零幻觉)
js
// cli/codegen/sync-props.mjs
// 输入:baseline 的 props.ts(标准 TS 文件)
// 输出:直接 patch target 的 t-input.types.ts
//
// 实现:
//  - 用 ts-morph 解析 baseline props.ts
//  - 把 baseline 没有 / target 有的字段 → 删除
//  - 把 baseline 有 / target 没有的字段 → 插入(保留注释、type、default)
//  - 字段按字典序排序
//  - 同时 patch t-input.uvue 里的 defineProps({}) runtime 声明
//
// 验证:跑完 → tsc --noEmit

这一步完全不需要 LLM,且 100% 准确。你之前的 audit 已经能列出 missing props,只需要再写"应用"这一步

CSS Token 提取/对比脚本(视觉对齐的最大公约数)
js
// cli/codegen/diff-css-tokens.mjs
// 输入:baseline.less + target.less
// 输出:
//   缺失 token:--td-input-bg-color, --td-input-label-color, ...
//   不一致默认值:--td-input-vertical-padding (baseline=16px, target=12px)
//
// LLM 只做一件事:根据这个表,把缺失的 token 加进 target 的 less
// 不需要 LLM 看截图,也不需要"猜"像素

这能把"视觉对齐"任务从"LLM 看图猜代码"变成"LLM 把表格转成 css 行"。LLM 转 schema 的准确率 > 95%,肉眼像素 < 50%。

TSC 反馈回路(self-heal)
js
// 每次 LLM patch 后必跑:
//   1. tsc --noEmit  → 收集编译错误(行号 + 信息)
//   2. 如果有错 → 把错误塞进 round-N+1 的 prompt 让 LLM 修
//   3. 3 轮自纠还过不了 → 回滚 + 标红等人介入

截图对比要不要保留?

保留,但降级用途

  • 最终验收信号(diffRatio 低于阈值 → 这一组 case 通过)
  • 不再作为 LLM 的输入(LLM 看图改像素 = 抽卡)

也就是说:截图变成测试,不是指令


我的诚实建议(直说)

当前 harness 路线,对于"视觉对齐"这件事是错的,对于"API 对齐"这件事是过度工程的。

推荐重构动作

  1. ✂️ 砍掉:把截图喂 LLM 的视觉修复链路(保留截图作为验收)
  2. 新增:cli/codegen/sync-props.mjs — 纯 ts-morph 脚本同步 props
  3. 新增:cli/codegen/diff-css-tokens.mjs — token 表对比
  4. 新增cli/harness/lib/typecheck.mjs — patch 后跑 tsc,错就回喂 LLM
  5. 🔧 改造:LLM prompt 不再含截图,只含"待补 props 表 + 待补 token 表 + 当前文件"
  6. 📐 新指标:不再追求 diffRatio < 0.5%(不可能达到),改为追求 "props 100% 对齐 + token 100% 覆盖 + tsc 0 error"——这三个都能确定性达到

时间预估

按这个新路线,单组件对齐时间从"半小时不收敛" → 3-5 分钟稳定收敛

  • props 同步:脚本 5s
  • token diff:脚本 5s
  • LLM 补 token:30s
  • tsc 验证:10s
  • 截图验收:30s

你想怎么走?

A)我推倒重来:把现有 fix 流程改成上面的新路线(保留截图模块作为验收,砍掉 LLM 看图)

B)渐进改良:先加 sync-props 脚本和 tsc 反馈回路,截图喂图先留着观察一两轮再决定砍不砍

C)你来定:你觉得视觉对齐这件事到底要不要追求 < 1% diff?这是路线分叉的关键。如果你只要求"看起来差不多",A 路线足够;如果你真要 < 1%,那只能放弃 LLM、人工设计 token。

我个人选 A,因为我现在能看到 LLM 在编 UniInputEvent 这种事,这种错误不会因为多跑几轮而消失——它是模型能力边界,不是 prompt 问题。

你定个方向,我马上开干。

uniapp x 同步模式

2026-06-15

/uniapp-x-components 这里有一些 uniapp x 的组件,但是 cli 模式没法直接运行。所以想把这个项目当作发 npm 包的工具,然后通过脚本同步到 /tdesign-uniapp-x-starter-hx 这个项目中。也就是 这个项目的 uniappx组件是源,改了后,在 tdesign-uniapp-x-starter-hx 中调试。

另外,之前做非uniappx ,也就是uniapp组件的时候,已经有一套同步机制了,npm run uniapp -- run dev, npm run uniapp -- run init 就有

架构方案分析

✅ 你这个架构合理,而且已经走在正确的路线上

我看了一下 packages/uniapp-x-components 的当前状态,发现你自己已经把这套机制搭了 80%

  • cli/sync-to-hbuilderx.mjs 已经实现了完整的"源 → HBuilderX 项目"同步链路
  • .sync-targets.json 多目标配置(gitignored,每台机器自己配)
  • pnpm sync / pnpm sync:watch / pnpm sync:dry 三种模式齐了
  • 而且做了关键的 .ts → .uts / .vue → .uvue 路径与扩展名映射

所以你问的不是 "要不要这么做",而是 "这个架构在 uni-app x 生态里是不是主流"。答案是:是主流,且是被迫只能这么做


🎯 为什么这个架构是合理的(甚至是唯一选择)

1. uni-app x 的硬约束:.uvue 离不开 HBuilderX
限制后果
.uvue 编译器闭源、只内嵌在 HBuilderX 里VSCode/cli 跑不起来真机
.uts 不是标准 TypeScript(语法子集 + 平台扩展)不能用 vitest/jest 直接跑
Vue3 SFC 工具链(vite/volar)对 uvue 支持极弱类型提示、单测、ESLint 都得绕路

结论:必须把"开发包"和"运行/调试包"物理拆开。

2. tang-ui / FirstUI-uvue 都是这个思路

刚才看了下两个参考:

  • FirstUI-uvue:本身就是个完整的 HBuilderX 项目,组件 + demo 页面 + tabbar 全在一个仓里,没有"独立组件库 + 独立 starter"的拆分。胜在简单,输给可测试性。
  • tang-ui:相对工程化,有 eslint.config.mjs、vite.config.ts、tailwind、husky,但 components/ 直接就在 HBuilderX 项目根下,也是单仓

而你目前的方案 = tang-ui 的工程化 + Shadcn registry 分发 + sync-to-hbuilderx 双仓 = 更彻底的工程化

3. 和你已有的非-x uniapp 同步机制一脉相承

npm run uniapp -- run dev 实际是 pnpm -C packages/tdesign-uniapp,那一套是根目录 monorepo 内 demo 项目的模式:组件在 packages/uniapp-components/,demo 在 packages/tdesign-uniapp/,靠 pnpm workspace 软链。

但 uniapp-x 没法走 workspace 软链——HBuilderX 不识别符号链接里的 uvue。所以你只能"复制 + 重写扩展名",也就是现在 sync-to-hbuilderx.mjs 干的事。


📐 推荐的最终架构

三个角色职责
仓 / 包角色必须有
packages/uniapp-x-components唯一可写源 + 测试 + 类型 + 文档.uvue.vue 镜像、.ts、vitest spec、registry build
tdesign-uniapp-x-starter-hx只读消费方 + demo pages + HBuilderX 真机调试components/ 全是 sync 出来的,不要手改
registry/*.json对外分发产物shadcn 兼容 schema

⚠️ 几个建议(针对你现有方案的小优化)

1. starter-hx 的 components/ 目录 .gitignore

既然内容是脚本生成,starter-hx 可以选两条路:

  • A. 不入 git.gitignorecomponents/,开发者第一次 clone 后必须先在主仓跑 pnpm sync 才能开 HBuilderX。推荐
  • B. 入 git 但禁止手改:在 components/ 顶部加 // AUTO-GENERATED FROM @tdesign/uniapp-x, DO NOT EDIT 头注释,pre-commit 校验未篡改。
2. 在 starter-hx 里加一个反向校验 hook

防止有人"在 starter-hx 里直接改了 uvue 但忘了同步回源仓"——

bash
# starter-hx 的 pre-commit
diff -r components/ ../tdesign-miniprogram/packages/uniapp-x-components/dist-hx/ || exit 1
3. sync 脚本可以增加"反向初始化"模式

类似你说的 npm run uniapp -- run init:在 starter-hx 里第一次启动时,跑一个脚本,自动从源仓 pnpm sync 一次。可以在 starter-hx 的 package.json 加:

json
"scripts": {
  "init": "node ../tdesign-miniprogram/packages/uniapp-x-components/cli/sync-to-hbuilderx.mjs",
  "dev": "pnpm init && echo '请在 HBuilderX 中打开本目录运行'"
}
4. 把现在的 .sync-targets.json 升级支持别名

当前每台机要写绝对路径。可以考虑读环境变量 TDESIGN_UNIAPP_X_STARTER 兜底,CI 上更顺手。

5. registry 和 sync 是两条互补路径,不冲突
  • sync:开发期,本地 HBuilderX 调试
  • registry:发布期,让别人 npx shadcn add @tdesign/uniapp-x/button 拷贝过去

两条都保留,不要二选一


🆚 备选方案对比

方案优点缺点评价
现方案:源 + sync + starter工程化好、可测试、可发包、双 IDE 协作需要维护 sync 脚本推荐
单仓(FirstUI 模式)零配置不能跑 vitest、不能发 npm、组件和 demo 耦合❌ 不适合一个组件库
pnpm workspace 软链改了立即生效HBuilderX 跟随软链有大量 bug(uvue 编译丢失、类型异常)❌ 实测不行
Git submodule starter-hx → 源仓 components版本可追反向依赖、修改流程别扭❌ 反人类
只发 npm 包,让 starter-hx pnpm i干净uvue 在 node_modules 里 HBuilderX 解析有坑、调试代码不能改了立刻看到⚠️ 发布后 OK,开发期不行

🧭 一句话结论

你这个架构合理且最佳,实质上就是"uni-app x 版的 m2w(miniprogram-to-web)双仓模式"。继续按现在的 sync-to-hbuilderx.mjs 走,把 .sync-targets.json 指到 /Users/guowangyang/Documents/github/tdesign-uniapp-x-starter-hx,然后 pnpm sync:watch 即可——和你旧的 npm run uniapp -- run dev 体验等价,且更适配 uni-app x 的生态约束。

要不要我帮你:

  1. .sync-targets.example.json 的 root 改成 starter-hx 的实际路径并生成 .sync-targets.json
  2. 在 starter-hx 加一个 npm run init 脚本反向触发同步?
  3. 给 starter-hx 加 components/ 的 .gitignore 和 README 说明"不要直接改这里"?

告诉我哪一项需要动手,我再操作(按规则避免擅自修改)。

2026-06-15

项目 - 项目设置 - 插屏设置,这里可以去掉 cocos 的首屏 logo,或者自定义 logo

leetcode/02-array-string/15-min-subarray-len.js 和 leetcode/05-dp/02-max-subarray.js 有啥区别

2026-06-15

这两道题虽然都涉及连续子数组,但本质差别很大:

对比总结

LeetCode 209 最小子数组长度LeetCode 53 最大子数组和
目标找和 ≥ target 的最短子数组长度和最大的子数组的和
返回值长度
数据约束全是正整数可含负数
核心方法滑动窗口动态规划(Kadane)
为什么能用该方法元素全正 → 窗口扩大时 sum 单调递增,可以贪心收缩左边界含负数无法用滑动窗口,需要 DP 决策「接上前面 vs 重新开始」

关键区别

  1. 滑动窗口 vs DP

    • 209 的元素全为正,所以右移 right 时 sum 只增不减,满足条件后收缩 left 即可 → 典型滑动窗口。
    • 53 含负数,窗口和不单调,无法用滑动窗口;用 Kadane 算法逐元素决策「是否从当前位置重新开始」。
  2. 优化的维度不同

    • 209 优化的是长度(找最短的满足条件的窗口)。
    • 53 优化的是(找和最大的子数组)。
  3. 判断逻辑

    • 209:while (sum >= target) 时缩窗,记录最小长度。
    • 53:curr = max(curr + nums[i], nums[i]),每步更新全局最大值。

简单记:209 是"定和求最短长度",53 是"不定长求最大和"

uniapp Vue3 组件多根问题

2026-06-13

⚠️ [Vue warn]: Extraneous non-props attributes (id) were passed to component but could not be automatically inherited because component renders fragment or text root nodes.

原因:调用栈 <Index> → <AsyncComponentWrapper> → <PageBody> → <Page>。uni-app 内部的 Page 组件给页面注入了一个 id 属性,但 vue3 透传需要单根节点;而我上一轮把 user/base 页的外层 <view :class :data-theme> 包裹去掉后,模板变成了多根

html
<NavBar />
<view class="home-container">...</view>
<CustomTabBar />
<ActivityFilterPopup />

vue 不知道把 id 透传到哪个根上,于是警告。

修复:恢复一个简单的单根 <view> 包裹(不带 class/data-theme,纯结构作用),把所有顶层节点包进去。

  • user/index.vue 整体重写 template,最外层单根 <view> 包住 NavBar、user-page、CustomTabBar
  • base/index.vue 在最外层加单根 <view> 包住 NavBar、home-container、CustomTabBar、ActivityFilterPopup

验证 chat-markdown 表格的横向滚动

2026-06-12

js
const markdownData = `
**普通表格**

| 左对齐     | 居中对齐 | 右对齐 | 内容 |
| :--------- | :------: | -----: | ----- |
| 单元格     |  单元格  | 单元格 | 单元格 |
| 长文本示例 | 长文本示例长文本示例长文本示例 |   $100 | 文本内容 |
| 文本示例   | 文本内容 | $100 |  文本内容 |

**超宽表格(可横向滚动)**

| 城市 | 销售额(万元) | 同比增长 | 环比增长 | 客单价 | 复购率 | 备注信息 |
| :--- | -----------: | :------: | :------: | -----: | -----: | :------- |
| 北京海淀 | 12345.67 | +18.5% | +3.2% | 256.78 | 42.3% | 一线核心商圈 |
| 上海浦东新区 | 23456.78 | +22.1% | +5.7% | 312.45 | 48.6% | 高端写字楼集群 |
| 广州天河 | 9876.54 | +12.3% | +1.8% | 198.32 | 38.1% | 老牌商业区 |
| 深圳南山 | 18765.43 | +25.6% | +6.1% | 289.67 | 45.9% | 科技创新中心 |
`;

验证 image-viewer 组件的 preventScrollThrough 属性

2026-06-12

vue
<!--
  [临时验证] preventScrollThrough 属性
  使用步骤:
   1. 滚动整个页面,让 ActionSheet 触发按钮位于屏幕中间或者上半部分
   2. 依次点击下方三个按钮,弹出 ActionSheet 后,把手指放在【遮罩层(半透明黑色背景)】上左右/上下滑动
   3. 对比:
        - true :底层页面不会跟着动(防止穿透 ✅)
        - false:底层页面会跟着滑动(穿透了 ❌,对照组)
        - 透传 popupProps.preventScrollThrough=false:组件仍优先取自身 preventScrollThrough,
          可对比验证 popup 的 prevent-scroll-through 是否被 actionSheet 透传/覆盖。
   4. 验证完毕后,直接删除本 demo 文件 + 删除 _example/action-sheet.vue 中临时验证区块即可。
-->
<template>
  <view>
    <!-- 上方占位长内容,制造可滚动的页面 -->
    <view class="filler">
      <view
        v-for="i in 30"
        :key="i"
        class="filler-line"
      >
        占位内容 {{ i }} —— 用于让页面可上下滚动,方便验证遮罩层是否阻止穿透
      </view>
    </view>

    <t-action-sheet
      :visible="visible"
      :prevent-scroll-through="preventScrollThrough"
      :popup-props="popupProps"
      :items="items"
      cancel-text="取消"
      @selected="onSelected"
      @close="onClose"
      @cancel="onClose"
    />

    <t-button
      size="large"
      variant="outline"
      block
      theme="primary"
      @click="open(true)"
    >
      preventScrollThrough = true(默认,应阻止穿透 ✅)
    </t-button>

    <t-button
      size="large"
      variant="outline"
      block
      theme="warning"
      @click="open(false)"
    >
      preventScrollThrough = false(穿透 ❌,对照组)
    </t-button>

    <t-button
      size="large"
      variant="outline"
      block
      theme="default"
      @click="openWithPopupProps()"
    >
      自身 true + popupProps.preventScrollThrough=false(透传场景)
    </t-button>

    <!-- 下方占位长内容 -->
    <view class="filler">
      <view
        v-for="i in 30"
        :key="i"
        class="filler-line"
      >
        占位内容 {{ i + 30 }}
      </view>
    </view>
  </view>
</template>

<script>
import TActionSheet from '@tdesign/uniapp/action-sheet/action-sheet.vue';
import TButton from '@tdesign/uniapp/button/button.vue';

export default {
  components: {
    TButton,
    TActionSheet,
  },
  data() {
    return {
      visible: false,
      preventScrollThrough: true,
      popupProps: {},
      items: ['选项一', '选项二', '选项三'],
    };
  },
  methods: {
    open(prevent) {
      this.preventScrollThrough = prevent;
      this.popupProps = {};
      this.visible = true;
      console.log('[ActionSheet] preventScrollThrough =', prevent);
    },
    openWithPopupProps() {
      this.preventScrollThrough = true;
      this.popupProps = { preventScrollThrough: false };
      this.visible = true;
      console.log('[ActionSheet] self=true, popupProps.preventScrollThrough=false');
    },
    onSelected(e) {
      console.log('[ActionSheet] selected:', e);
      this.visible = false;
    },
    onClose() {
      this.visible = false;
    },
  },
};
</script>

<style lang="less" scoped>
.filler {
  padding: 16rpx 0;
}
.filler-line {
  padding: 24rpx 32rpx;
  font-size: 28rpx;
  color: #888;
  border-bottom: 1rpx solid #eee;
}
</style>

删除多余的 pages

2026-06-12

js
[...document.querySelectorAll('tr')]
  .map(item => item.querySelectorAll('td')[0])
  .filter(Boolean)
  .map(item => item.innerText.trim())
  .filter(item => item.startsWith('pmd-ai-e2e-') && item!== 'pmd-ai-e2e.pages.woa.com')

2的n次方和n的阶乘,哪个大

2026-06-11

n! 比 2ⁿ 大(当 n 足够大时)。

直观对比

n2ⁿn!
242
386
41624 ← 从这里开始 n! 超过 2ⁿ
532120
101,0243,628,800
20~10⁶~2.4×10¹⁸

为什么

把两者都写成连乘的形式:

  • 2ⁿ = 2 × 2 × 2 × ... × 2(n 个 2 相乘)
  • n! = 1 × 2 × 3 × ... × n(从 1 乘到 n)

当 n ≥ 4 时,n! 的大部分因子(3, 4, 5, ..., n)都比 2 大,所以 n! 增长远快于 2ⁿ。

增长率排序

常见复杂度从小到大:

O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!) < O(nⁿ)

所以全排列 O(n × n!) 比子集 O(n × 2ⁿ) 慢得多,这也符合直觉——n 个元素的排列数 n! 远多于子集数 2ⁿ。

子集问题时间复杂度 O(n × 2ⁿ) 的推导

2026-06-11

1. 子集总数

对于 n 个元素,每个元素有「选」或「不选」两种状态,所以子集总数 = 2ⁿ

[1,2,3] 为例:2³ = 8 个子集。

2. 递归树的结构

                  []
          /        |        \
        [1]       [2]       [3]
       /   \       |
    [1,2]  [1,3] [2,3]
      |
   [1,2,3]

每个节点都是一个合法子集,所以树的节点总数 = 2ⁿ

3. 每个节点的开销

每进入一次 backtrack,都执行 result.push([...path]),拷贝当前 path。

path 的平均长度是多少?所有子集的元素总数 = Σ(k=0 到 n) k × C(n,k) = n × 2ⁿ⁻¹

所以平均每次拷贝长度 = n/2,量级为 O(n)。

4. 汇总

部分次数单次开销小计
backtrack 调用2ⁿO(1)(循环本身)O(2ⁿ)
拷贝 path 到结果2ⁿ平均 O(n/2) → O(n)O(n × 2ⁿ)

主导项是拷贝操作,总时间复杂度 = O(n × 2ⁿ)

5. 直觉理解

输出共 2ⁿ 个子集,所有子集的元素总数为 n × 2ⁿ⁻¹,光是写出所有答案就需要 O(n × 2ⁿ) 时间,这就是下界。

6. 对比全排列

问题结果数每个结果长度总复杂度
全排列n!nO(n × n!)
子集2ⁿ平均 n/2O(n × 2ⁿ)

两者的模式一样:结果数 × 单个结果长度

全排列时间复杂度 O(n × n!) 的推导

2026-06-11

1. 递归树的结构

[1,2,3] 为例,回溯形成一棵决策树:

第 0 层:n = 3 个分支选择
第 1 层:n-1 = 2 个分支选择
第 2 层:n-2 = 1 个分支选择(叶子节点)

2. 计算总节点数(即 backtrack 调用次数)

每层的节点数:

  • 第 0 层:1 个节点,产生 n 个分支
  • 第 1 层:n 个节点,每个产生 n-1 个分支
  • 第 2 层:n×(n-1) 个节点
  • ...
  • 第 k 层:n!/(n-k)! 个节点(即 P(n,k))

总节点数 = Σ(k=0 到 n) n!/(n-k)! = n! × Σ(k=0 到 n) 1/k!

由于 Σ(1/k!) 趋近于 e ≈ 2.718,所以总调用次数 ≈ e × n!,量级仍是 O(n!)

3. 每个叶子节点的额外开销

到达叶子时执行 result.push([...path]),拷贝长度为 n 的数组,开销 O(n)

叶子节点恰好有 n! 个(就是全排列的个数)。

4. 汇总

部分次数单次开销小计
非叶节点的循环 + 选择/撤销O(n!)O(1)O(n!)
叶子节点拷贝结果n!O(n)O(n × n!)

主导项是叶子节点的拷贝,因此总时间复杂度 = O(n × n!)

5. 直觉理解

你最终输出了 n! 个排列,每个排列长度为 n,光是写出所有答案就需要 O(n × n!) 的时间,这是下界,算法不可能比这更快。

所以代码注释里写的 O(n × n!) 就是这么来的。

微信安全防护

2026-06-11

微信安全防护: https://developers.weixin.qq.com/miniprogram/security/gateway/config/security.html

综合账号身份、设备、用户行为特征、环境等小程序场景下多重维度数据分析。

可以接入微信安全插件,他会给出风险等级和当前设备登录当前小程序的UV数量,就是看这个设备是不是专门用来刷的,可以根据风险等级进行相应的验证码、短信验证。

对比清晰模式和庞杂模式

2026-06-10

在项目上完全胜任的成员可能处在庞杂的认知模式下(Complicated),他们完成任务的行为是:感知(理解用户故事的上下文,以及验收条件)- 分析(按照测试工序指引,分解任务)- 响应(依据任务列表逐步完成工作)

项目上不完全胜任的成员可能处于复杂的认知模式下(Complex),他们完成任务的行为是:探测(尝试 spike 一下故事卡中某些不清楚的地方)- 感知(按照得到的结果,重新理解要如何完成整张卡片)- 响应(逐步试错,完成功能)

而刚毕业没有什么编程经验的成员,可能完全就是混乱模式了。他们甚至不会仔细辨别到底要做什么功能,就被巨大的恐慌驱动,冲过去写代码了。那么带来的自然是大量的返工和修改。

对比清晰模式和庞杂模式的区别,清晰模式的关键是分类(categorize),庞杂模式的关键是分析(analyze)。那么这句是什么意思呢?其实是说我们检查软件质量时,主要应该依赖自动化测试,只有在极少的情况下,才需要手动测试。

CI/CD

2026-06-10

持续构建发布、持续质量保障(eslint/ts)、持续自动化协作(白名单审核、pmd-mcp更新)、持续运维和监控(磁盘)、持续安全治理(审核)

CI 和 CD 的区别

2026-06-10

一、字面定义

缩写全称中文
CIContinuous Integration持续集成
CDContinuous Delivery / Continuous Deployment持续交付 / 持续部署

注意 CD 有两个含义,很多人混淆就在这里。


二、各自负责什么

CI(持续集成)—— 解决"代码合得进去吗?"

开发者每次提交代码后,自动完成:

拉代码 → 安装依赖 → Lint → 单元测试 → 构建产物

目标:尽早发现集成冲突和质量问题,保证 main 分支随时是绿的。

CD - Continuous Delivery(持续交付)—— 解决"代码随时能发吗?"

在 CI 基础上,自动把构建产物发布到类生产环境(预发/灰度/staging),并准备好可发布的制品。

关键:最后上生产那一步是手动点按钮(人为审批)。

CD - Continuous Deployment(持续部署)—— 解决"代码自动上线"

比持续交付更激进一步:通过所有自动化测试后,直接自动发到生产,全程无人工干预。


三、流程对比图

三者关系:CI ⊂ Continuous Delivery ⊂ Continuous Deployment(后者包含前者)。


四、本质区别(核心)

维度CICD (Delivery)CD (Deployment)
关注点代码质量发布能力发布动作
解决的问题"合并不冲突、测试通过""随时可发布""随时已发布"
产物终点构建产物(artifact)预发/staging 环境生产环境
人工干预(决定何时上线)
价值对象开发团队业务/产品决策终端用户
失败的代价重新提交即可回滚 staging影响线上用户

一句话本质区别

  • CI 关心"代码能不能被信任"(质量门禁)
  • CD 关心"产品能不能被交付"(部署管道)

CI 是面向代码的工程实践,CD 是面向价值流的业务实践。CI 让你"敢合代码",CD 让你"敢发版本"。


五、结合你项目的实例

看你 bk 目录里的脚本就是典型 CI/CD 流水线拆分:

文件阶段类型
bk/12.npm-publish/3.auditor.js(审核)代码合并前检查CI
bk/12.npm-publish/4.check-audit.js测试 / 构建CI
bk/4.cos-all-in-one/5.upload-to-cos.js(上传 CDN)产物分发到 CDNCD
bk/3.gray-publish/*(灰度发布)灰度上线、人工确认CD-Delivery
bk/1.all-in-one-publish.backend/*(k8s 镜像构建+部署)自动部署到 BCSCD-Deployment
bk/14.HoK 商户赛部署(用 BCS 模板发布)部署生产环境CD

可以看到:CI 主要是 lint/test/build/audit;CD 是 cos 上传、镜像 push、bcs 滚动更新这些"动产物"的事情。


六、容易混淆的点

  1. "自动化 ≠ CI/CD":写个脚本一键发布只是自动化,频率高 + 触发自动 + 有质量门禁才叫 CI/CD。
  2. CI 不是测试:CI 包含测试,但本质是集成——多人代码频繁合到主干。
  3. 没有 CI 就谈 CD 是耍流氓:CD 必须建立在可靠的 CI 之上,否则只是把 bug 更快地推给用户。
  4. DevOps ≠ CI/CD:CI/CD 是 DevOps 的工程实践之一,DevOps 还包括监控、IaC、协作文化等。

如果只让我用一句话总结:

CI 是把代码持续地"集成进主干",CD 是把主干持续地"交付给用户"。前者保证写得对,后者保证发得快。

找到环的入口节点 这个不理解,为什么 fast 是 a+b+b+c

2026-06-10

图解 fast 走过的路径

先看链表结构:

head ——→ ... ——→ 入口 ——→ ... ——→ 相遇点 ——→ ... ——→ (回到入口)
|←———— a ————→|←—— b ——→|←———— c ————→|
  • a:head 到环入口的距离
  • b:环入口到相遇点的距离
  • c:相遇点再走回环入口的距离
  • 环长 = b + c

slow 走过的路径

slow 进入环后,还没走完一圈就会被 fast 追上(上面证明过),所以:

slow 走的距离 = a + b

fast 走过的路径

fast 在 slow 进入环之前,已经在环里转了若干圈。关键点:

  • fast 也走了 a 步到达入口
  • 然后在环里转,到相遇时 fast 走过的环内距离是 b + n圈

取最简情况 n=1(fast 比 slow 多走了恰好一圈环):

fast 走的距离 = a + b + (b + c) = a + 2b + c

其中 (b + c) 就是多走的那一整圈环。

为什么是多走一圈?

因为 fast 速度是 slow 的 2 倍:

fast的距离 = 2 × slow的距离
a + 2b + c = 2(a + b)

解方程:

a + 2b + c = 2a + 2b
        c = a        ← 关键结论!

更严谨地说

如果 fast 多走了 n 圈(n ≥ 1):

fast = a + b + n(b + c)
2(a + b) = a + b + n(b + c)
a + b = n(b + c)
a = n(b + c) - b = (n-1)(b+c) + c

a = (n-1)圈 + c

所以从 head 走 a 步 = 从相遇点走 c 步 + 转 (n-1) 整圈,最终都停在环入口。这就是为什么让两个指针分别从 head 和相遇点出发、各走一步,一定在入口相遇。

一句话总结

a + 2b + c 的含义是:走 a 步到入口 → 走 b 步到相遇点 → 继续走完环剩下的 c 步回到入口 → 再走 b 步到相遇点,即 fast 在环里多绕了一圈才和 slow 碰上。

lengthOfLIS2 详解——贪心 + 二分

2026-06-10

核心思想

我们维护一个辅助数组 tails,其中 tails[i] 表示:所有长度为 i+1 的递增子序列中,末尾元素的最小值

为什么要"最小末尾"?因为末尾越小,后面能接上更大元素的机会就越大——这就是贪心策略。

tails 数组的关键性质

tails 始终是严格递增的。原因直觉上很好理解:更长的递增子序列,其末尾一定比更短的那个大。

算法流程

遍历 nums 中的每个 num,做一件事:tails 里二分查找第一个 ≥ num 的位置 l,然后 tails[l] = num

这会产生两种情况:

情况含义
l === tails.lengthnumtails 所有元素都大,追加到末尾,LIS 长度 +1
l < tails.length替换 tails[l],让长度为 l+1 的子序列末尾变得更小(贪心优化),LIS 长度不变

用示例走一遍 [10, 9, 2, 5, 3, 7, 101, 18]

步骤num二分找到位置 l操作tails 状态
1100(空数组)追加[10]
290(9 < 10,替换)替换[9]
320(2 < 9,替换)替换[2]
451(5 > 2,追加)追加[2, 5]
531(3 < 5,替换)替换[2, 3]
672(7 > 3,追加)追加[2, 3, 7]
71013(101 > 7,追加)追加[2, 3, 7, 101]
8183(18 < 101,替换)替换[2, 3, 7, 18]

最终 tails.length = 4,即 LIS 长度为 4。

二分部分逐行解释

42:55:leetcode/05-dp/09-longest-increasing-subsequence.js
function lengthOfLIS2(nums) {
  const tails = [];
  for (const num of nums) {
    let l = 0;
    let r = tails.length;       // 右边界是开区间
    while (l < r) {
      const m = (l + r) >> 1;   // 取中点
      if (tails[m] < num) l = m + 1;  // mid 太小,往右找
      else r = m;               // mid >= num,收缩右边界
    }
    tails[l] = num;             // l 就是第一个 >= num 的位置
  }
  return tails.length;
}

这是标准的左边界二分(lower_bound):找到 tails 中第一个 ≥ num 的下标。

注意点

  • tails 不是真正的 LIS 序列(只保证长度正确,元素可能被替换过)。
  • 时间复杂度 O(n log n):外层遍历 O(n),内层二分 O(log n)。
  • 空间复杂度 O(n)tails 最长和 nums 等长。

JS 中的 >> 1

2026-06-10

>> 1位运算右移 1 位,等价于 除以 2 并向下取整(即 Math.floor(x / 2))。

在代码中的作用

49:49:leetcode/05-dp/09-longest-increasing-subsequence.js
const m = (l + r) >> 1;

这里就是计算二分查找的中间下标m = Math.floor((l + r) / 2)

为什么用 >> 1 而不是 / 2

  1. 性能微优:位运算比除法快(现代引擎差异极小,更多是习惯写法)
  2. 自动取整>> 1 天然丢弃小数位,不需要额外调用 Math.floor()
  3. 避免浮点/ 2 在 JS 中返回浮点数(如 7 / 2 = 3.5),而 >> 1 直接得到整数 3

几个例子

表达式二进制过程结果
6 >> 11100113
7 >> 11110113(向下取整)
10 >> 1101001015

总结(l + r) >> 1 就是取 lr 的中点,是二分查找的标准写法。

二叉树遍历

2026-06-10

前序:根→左→右(用于复制树、序列化) 中序:左→根→右(BST 中输出有序序列) 后序:左→右→根(用于删除树、计算表达式)

子集和全排列的关键区别

2026-06-10

  • 全排列:每层都从 0 遍历,用 used 标记避免重选同一个元素
  • 子集:通过 start 参数只往后选,保证 [1,2] 和 [2,1] 不会重复出现,且每进入一次函数就收集结果(因为任何长度都是合法子集)

searchLeftBound, 每次优先从左边判断,就会拿最左边的数字,这句话对吗

2026-06-10

这句注释不太准确,容易产生误导。

searchLeftBound 能找到最左边的 target,核心原因不是"优先从左边判断",而是:

nums[mid] >= target 时,不立即返回,而是收缩右边界 right = mid,继续往左搜索。

具体来说:

  • nums[mid] < targetleft = mid + 1(排除 mid,往右找)
  • nums[mid] >= targetright = mid(mid 可能是答案,但不排除左边还有更早的,所以继续往左逼近)

这个"找到了也不停,继续往左压缩"的行为,才是它能拿到最左边那个 target 的原因。

对比 searchRightBound,它把条件分界改为 nums[mid] <= targetleft = mid + 1,即找到了也不停、继续往右压缩,所以拿到的是最右边。

建议把那行注释改为更准确的描述: 总结一下:

说法正确性
"优先从左边判断" → 拿最左边❌ 不准确
找到 >=target 时不停,继续收缩右边界往左逼近 → 拿最左边✅ 正确

关键区别在于 else { right = mid } 这一步——它让算法在"可能已经找到答案"时仍然不停下来,继续向左压缩搜索区间,直到区间为空,此时 left 就是第一个 >= target 的位置。