@alipay/alipay-aipay 1.7.8 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +2 -2
  2. package/package.json +3 -3
  3. package/skill/SKILL.md +34 -3
  4. package/skill/VERSION +1 -1
  5. package/skill/references/normal/conversation-telemetry.md +103 -0
  6. package/skill/references/normal/json/customer-messages.json +1 -1
  7. package/skill/references/normal/json/policy-rules.json +1 -1
  8. package/skill/references/normal/scripts/conversation_consent.mjs +76 -0
  9. package/skill/references/normal/scripts/conversation_telemetry.mjs +200 -0
  10. package/skill/references/normal/scripts/open_official_url.sh +4 -2
  11. package/skill/references/normal/scripts/render_customer_message.mjs +6 -2
  12. package/skill/references/normal/scripts/runtime.mjs +1 -0
  13. package/skill/references/normal/scripts/self_update.mjs +6 -7
  14. package/skill/references/normal/scripts/telemetry.mjs +618 -74
  15. package/skill/references/normal/scripts/windows/bootstrap-node.ps1 +0 -2
  16. package/skill/references/runtime-entry.md +7 -1
  17. package/skill-pre/VERSION +1 -1
  18. package/skill-pre/references/normal/conversation-telemetry.md +9 -9
  19. package/skill-pre/references/normal/scripts/conversation_telemetry.mjs +113 -18
  20. package/skill-pre/references/normal/scripts/runtime.mjs +4 -4
  21. package/skill-pre/references/normal/scripts/self_update.mjs +6 -7
  22. package/skill-pre/references/normal/scripts/telemetry.mjs +243 -32
  23. package/skill-pre/references/normal/scripts/windows/git-discovery.mjs +6 -2
  24. package/skill-pre/references/normal/scripts/windows/git-install.mjs +2 -3
  25. package/skill-pre/references/normal/scripts/windows/preflight.mjs +13 -2
  26. package/skill-pre/references/normal/scripts/windows/prepare.mjs +2 -2
  27. package/skill-pre/references/normal/scripts/windows/routing.mjs +4 -1
  28. package/skill-pre/references/normal/windows-environment.md +4 -4
@@ -158,8 +158,6 @@ function Invoke-NodeBootstrap([string]$Action, [string]$Root) {
158
158
  if ($MyInvocation.InvocationName -ne '.') {
159
159
  $ErrorActionPreference = 'Stop'
160
160
  try {
161
- . (Join-Path $PSScriptRoot 'architecture.ps1')
162
- if ((Get-AipayNativeArchitecture) -ne 'x64') { throw 'UNSUPPORTED_PLATFORM' }
163
161
  $localData = $env:LOCALAPPDATA
164
162
  if (-not $localData) { $localData = [Environment]::GetFolderPath('LocalApplicationData') }
165
163
  if (-not $localData -or -not [IO.Path]::IsPathRooted($localData)) { throw 'STORAGE_FAILED' }
@@ -52,7 +52,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry resolve-sessi
52
52
 
53
53
  从首条 bootstrap/start-run 起,本文件及所有 flow 登记的每条 runtime 命令均追加 `--agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"`,将两个占位符分别替换为当前工具名称和根入口已取得的同一会话 ID;不使用环境变量前缀,不依赖前一次工具调用的进程环境。gateway 在业务参数解析前剥离此公共参数,规范化后传给当前进程及子进程。业务 CLI 的 `PLATFORM` 和遥测 Yuyan `d2` 优先使用本次命令的 Agent 宿名,只有该值为 `unknown` 或非法时才检查当前进程的宿主自动识别信号;继承的通用平台变量只能作为末级兼容值。这样即使进程同时残留其他宿主的 session/thread 环境变量,也不得遮蔽本次 runtime 明确提供的当前宿主。所有来源中的 `unknown` 或非法值都继续回退,不得提前终止识别。该兜底不使用宿主名称枚举或映射表:纯 ASCII 名称统一小写并去除空格、下划线和连字符,合法 Unicode 名称保留可读原文;无法安全规范化仍为 `unknown`。该参数不改变业务确认、run_id、PLATFORM_ID 或模型字段。
54
54
 
55
- 根 `SKILL.md` 启动器已经在读取本文件前确定本轮有效版本,并完成本对话所需的首次 `env refresh-alipay-cli` 或命中同路径、同版本的成功刷新事实。运行时禁止再次执行自更新、读取 `normal/self-update.md` 或切换其他副本;成功事实仍有效时不重复刷新 CLI,只按原始请求识别根意图并加载入口:
55
+ 根 `SKILL.md` 启动器已经在读取本文件前确定本轮有效版本,并完成本对话的采集说明及明确选择(同意采集或仅使用服务),以及本对话所需的首次 `env refresh-alipay-cli` 或命中同路径、同版本的成功刷新事实。运行时禁止再次执行自更新、读取 `normal/self-update.md` 或切换其他副本;成功事实仍有效时不重复刷新 CLI,只按原始请求识别根意图并加载入口:
56
56
 
57
57
  | 意图 | 判定 | 入口 | 后续按需加载 |
58
58
  |---|---|---|---|
@@ -70,6 +70,12 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry resolve-sessi
70
70
 
71
71
  遥测默认开启。`ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 时不计数,不生成 event/c4/进度,不读写 outbox/status,不上传、不启动 worker、不 flush;只允许保留 `PLATFORM_ID <- run_id` 所需的最小私有 run context。失败静默,不改变业务、消息、确认或卡点,内部 marker 不对客。`integration_only` 由 `integration_summary` 终结;`onboarding_only` 由 `onboarding_summary` 终结;`full_process` 由 `all_summary` 终结,`full_process` 的 `integration_summary` 只是中间 event。每个 stage 先在 run state 固定唯一 event,再写 outbox并同步上传,网络硬超时 3 秒;失败保留原 event 后才尝试 detached worker,绝不延迟重算 c4。补传细节见 `normal/telemetry.md`。
72
72
 
73
+ 对话采集在用户明确同意后,以触发 Skill 的那条用户消息为起点,按原始顺序补采授权前的完整对话(含采集说明、澄清往来等助手收口消息),每条只采集一次、不重复采集,使用独立的 `conversation_id`,不携带或关联业务 `run_id`。对话遥测环境开关默认可用,但实际采集还必须取得当前会话的明确同意;`ALIPAY_AIPAY_CONVERSATION_TELEMETRY=0|false|off|no` 时只停用对话采集,不影响阶段遥测。阶段遥测继续使用 code=1005;对话遥测默认使用独立 code=1008,可由 `ALIPAY_AIPAY_CONVERSATION_CODE` 覆盖,非法值或与 1005 相同则停用对话采集。
74
+
75
+ 当前会话已明确同意、全局遥测开启且对话 code 有效时,读取 `normal/conversation-telemetry.md` 的采集规则。每轮采集本轮用户消息,以及阻塞式对客消息和最终收口助手回复;过程输出、工具结果和非阻塞阶段消息跳过。用户消息先执行原有 `count-user-input`,再采集。助手消息必须在发送前采集,采集副本仅替换随后发送正文中的凭证和个人信息,其余文字与格式保持一致;不得为满足逐字一致而上传敏感原文。
76
+
77
+ 运行时命令使用 `$CONVERSATION_ID` 和 `$RUN_ID` 作为 Shell 变量;协议字段统一写为 `conversation_id` 和 `run_id`。对话采集不关联业务 run,不上传工具输出、system 消息、隐藏推理或附件内容。任务完成、明确取消或退出时,先采集最终回复,再执行 `telemetry end-conversation`。采集失败静默,不阻断 Skill。
78
+
73
79
  用户输入计数是入口前置职责,只从 `clarify_intent` 之后开始,到当前 `run_id` 的 `summary` 收口为止。当前 active run 期间,聊天框里**每收到一条用户消息都必须先计一次**,再做任何 Skill 相关或非 Skill 相关处理;计数不按是否与 Skill 流程强相关筛选。只要是用户在 Agent 对话框发送的消息,就按一条计,不计宿主/系统/网页授权、扫码、Skill stage、runner、工具调用或自动轮询。文字、图片/文件、空白/短确认、材料补充、确认回复和中途插话都按一条用户消息计;同一条消息含文字和图片/文件仍只计一条。`USER_INPUT_ID` 是当前 active run 内用户消息的本地幂等键:Agent 给每条新用户消息分配对话内单调序号,同一消息重试复用同一值。`USER_INPUT_ID` 不得包含或派生自用户输入内容、文件名、路径、截图/OCR、业务材料、stage 名或工具调用序号;运行时只保存其本地 hash,不上报。
74
80
 
75
81
  `start-run` 的 `clarify_intent` 开始生成后计数器为 0,直到当前 run 最终 summary 到达。Agent 每收到一条聊天框用户消息,都必须先分配不含消息内容的 `USER_INPUT_ID`,执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry count-user-input --run-id "$RUN_ID" --input-id "$USER_INPUT_ID" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"`,再解释、分类、路由、调用 runner/tool 或回复;不论消息是否与 Skill 相关,文字、图片/文件、空白、确认、中途插话和打断后输入都执行。同一消息重试复用同一 ID,runtime 只保存 `run_id + USER_INPUT_ID` hash。无 active run、缺少/错误 run ID 或 run 已关闭时成功 no-op。runner、recovery、renderer、交互回复、tool call/result 和 stage 禁止隐式计数;它们保留的业务 `--input-id` 也不是计数兜底。宿主总入口不在仓库内,因此这是 Agent 收到消息后必须遵循的唯一显式计数动作,不增加用户操作。
package/skill-pre/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.7-pre
1
+ 1.7.9-pre
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 开关与范围
4
4
 
5
- 对话遥测环境开关默认可用,实际采集必须先取得当前宿主会话的明确同意,可用独立开关 `ALIPAY_AIPAY_CONVERSATION_TELEMETRY=0|false|off|no` 关闭;未设置或为其他值时保持开启。该开关只关闭对话采集,不影响阶段遥测。默认 code 见下方上报字段表。可用 `ALIPAY_AIPAY_CONVERSATION_CODE` 覆盖(1–16 位数字,不得与阶段遥测 code 数值相同);显式空值、非法值或与阶段遥测 code 数值相同时停用对话入口,不影响阶段遥测。全局 `ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 时,两类遥测均关闭,对话采集入口不采集、不读写、不发送;用户主动拒绝仍保存最小隐私选择,明确拒绝的例外见下一段。每条保留 d1~d5,c1 为 conversation_id,c2 仅含 sequence、role、content。正文仅脱敏凭证和个人信息,不设置采集文本或消息 JSON 的人为长度上限,不截断正文;本地缓存沿用阶段遥测的现有规则。阶段遥测原有 run 创建/关闭、计数和进度不变。
5
+ 对话遥测环境开关默认可用,实际采集必须先取得当前宿主会话的明确同意,可用独立开关 `ALIPAY_AIPAY_CONVERSATION_TELEMETRY=0|false|off|no` 关闭;未设置或为其他值时保持开启。该开关只关闭对话采集,不影响阶段遥测。默认 code 见下方上报字段表。可用 `ALIPAY_AIPAY_CONVERSATION_CODE` 覆盖(1–16 位数字,不得与阶段遥测 code 数值相同);显式空值、非法值或与阶段遥测 code 数值相同时停用对话入口,不影响阶段遥测。全局 `ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 时,两类遥测均关闭,对话采集入口不采集、不读写、不发送;用户主动拒绝仍保存最小隐私选择,明确拒绝的例外见下一段。每条保留 d1~d5,c1 为 conversation_id,c2 仅含 sequence、role、content。正文过滤凭证、个人信息及原流程禁止记录的公钥和临时 URL,不设置采集文本或消息 JSON 的人为长度上限,不截断正文;本地缓存沿用阶段遥测的现有规则。阶段遥测原有 run 创建/关闭、计数和进度不变。
6
6
 
7
7
  明确的用户拒绝属于隐私选择:即使遥测环境开关当前关闭,preview consent 的“仅使用服务”和 telemetry opt-out 仍保存最小拒绝状态并关闭已有采集,防止开关恢复后旧授权复活;这不采集正文、不生成事件或发起上传。环境开关关闭时的同意不写授权,恢复开关后仍需有效的明确同意。
8
8
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  `SESSION_ID` 复用启动器处理声明选择时一次取得的稳定身份,后续采集和业务 runtime 命令均显式携带同一个值。宿主无身份时,启动器的 resolve-session 生成一次随机身份并沿用,不能为对话与业务分别生成。无法恢复身份时重新展示声明并取得选择,不能继承旧会话同意。上传的 `conversation_id` 仍由运行时生成独立随机 UUID,不等于 SESSION_ID 或业务 RUN_ID。
16
16
 
17
- 首次采集以触发 Skill 的那条用户消息为起点。用户同意采集时,按原始顺序补采授权前的完整对话:触发消息为第一条(sequence=1),随后依次是内测声明、澄清往来(如有)和同意回复,每条只采集一次,不重复采集。授权前的助手收口消息(声明、澄清)与正常轮次一样带 `--final true` 采集;其 `MESSAGE_ID` 由对应用户消息的幂等键确定性派生(如 `<触发幂等键>#notice`、`<澄清回复幂等键>#clarify`),同一对话内不得与其他消息重复。授权前内容无法可靠取得(如上下文压缩)时,从可可靠取得的最早消息开始,禁止伪造。`MESSAGE_ID` 使用消息的稳定非内容型身份,同一消息重试复用。`SANITIZED_MESSAGE_JSON` 是 `{"role":"user","content":"脱敏后的消息","has_attachments":false}`;只替换凭证与个人信息,保留其余文字及格式。原文不得拼入命令或普通临时文件,使用结构化参数安全传递脱敏 JSON。无文字附件用空文本和 true;不读取附件、OCR 或文件名。运行时兼容 `{"text":"..."}`,其余非法结构静默拒绝。
17
+ 首次采集以触发 Skill 的那条用户消息为起点。用户同意采集时,按原始顺序补采授权前的完整对话:触发消息为第一条(sequence=1),随后依次是内测声明、澄清往来(如有)和同意回复,每条只采集一次,不重复采集。授权前的助手收口消息(声明、澄清)与正常轮次一样带 `--final true` 采集;其 `MESSAGE_ID` 由对应用户消息的幂等键确定性派生(如 `<触发幂等键>#notice`、`<澄清回复幂等键>#clarify`),同一对话内不得与其他消息重复。授权前内容无法可靠取得(如上下文压缩)时,从可可靠取得的最早消息开始,禁止伪造。`MESSAGE_ID` 使用消息的稳定非内容型身份,同一消息重试复用。`SANITIZED_MESSAGE_JSON` 是 `{"role":"user","content":"脱敏后的消息","has_attachments":false}`;过滤凭证、个人信息及原流程禁止记录的公钥和临时 URL,保留其余文字及格式。原文不得拼入命令或普通临时文件,使用结构化参数安全传递脱敏 JSON。无文字附件用空文本和 true;不读取附件、OCR 或文件名。运行时兼容 `{"text":"..."}`,其余非法结构静默拒绝。
18
18
 
19
19
  ```text
20
20
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry start-conversation --session-id "<SESSION_ID>" --message-id "$MESSAGE_ID" --message-json "$SANITIZED_MESSAGE_JSON"
@@ -36,7 +36,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry opt-out --ses
36
36
 
37
37
  当前授权仍有效且采集 ACTIVE 时,以对话轮次为单位只采集每轮的头和尾:头是本轮用户消息,逐条采集当前宿主会话内的所有用户消息;尾是本轮的阻塞对客消息与最终收口助手回复;不按是否与支付或 Skill 业务相关筛选。包括澄清、补材料和临时切换话题的用户消息;同一轮内的过程说明、进行中回复、阶段性助手回复、工具结果和轮内送达的非阻塞登记对客消息(如方案、摘要、checklist 结果)一律跳过;切换话题本身不等于取消或退出。
38
38
 
39
- 助手侧采集对象按 Skill 既有阻塞/非阻塞概念判定,不另设分类:凡阻塞对客消息(送达后本轮即停止、等待用户回复:上下文补问、MCC/应用/服务候选选择、材料收集、阻塞确认、授权或公钥页 pending 等)一律采集;每轮最终收口回复一律采集。阻塞消息通常同时就是本轮收口回复,按一条采集,不重复。两类都适用下方"先采集、后发送"规则:带 `--final true` 的 `conversation-message` 是送达前必须执行的采集动作(任务结束时仅允许再执行下文的 end-conversation),业务交付屏障"送达前禁止调用工具"不排除本条采集,采集内容是最终正文仅替换凭证和个人信息后的版本,不改写其他内容。正文不含 system、隐藏推理或工具输出;不能把工具 stdout 当已送达的回复。已有业务计数窗口内,先执行原有 count-user-input,再采集;窗口外采集对话不补计 counter,所有交付屏障和原计数顺序优先。后续消息执行:
39
+ 助手侧采集对象按 Skill 既有阻塞/非阻塞概念判定,不另设分类:凡阻塞对客消息(送达后本轮即停止、等待用户回复:上下文补问、MCC/应用/服务候选选择、材料收集、阻塞确认、授权或公钥页 pending 等)一律采集;每轮最终收口回复一律采集。阻塞消息通常同时就是本轮收口回复,按一条采集,不重复。两类都适用下方"先采集、后发送"规则:带 `--final true` 的 `conversation-message` 是送达前必须执行的采集动作(任务结束时仅允许再执行下文的 end-conversation),业务交付屏障"送达前禁止调用工具"不排除本条采集,采集内容是最终正文仅替换凭证、个人信息及原流程禁止记录的公钥和临时 URL后的版本,不改写其他内容。正文不含 system、隐藏推理或工具输出;不能把工具 stdout 当已送达的回复。已有业务计数窗口内,先执行原有 count-user-input,再采集;窗口外采集对话不补计 counter,所有交付屏障和原计数顺序优先。后续消息执行:
40
40
 
41
41
  ```text
42
42
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry conversation-message --session-id "<SESSION_ID>" --conversation-id "$CONVERSATION_ID" --message-id "$MESSAGE_ID" --message-json "$SANITIZED_MESSAGE_JSON"
@@ -44,7 +44,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry conversation-
44
44
 
45
45
  用户暂时离开、等待补材料或审核、澄清、遇到报错或中间阶段完成都不结束采集;之后在同一宿主对话继续时沿用原 conversation_id。自更新后保留当前标识,后续命令使用新版同一路径。产品已明确时,初始化和后续消息命令可追加 `--product-type aipay|webpay|apppay`;未知则省略,d5=UNKNOWN。不读取或关联业务 run;首次授权前的补采只在上述明确同意后按原始顺序执行,后续持续采集不回填遗漏消息。
46
46
 
47
- 助手最终收口回复固定采用先采集、后发送:Agent 组织好本轮最终收口回复后,先执行上述 `conversation-message` 并追加 `--final true`,`--message-json` 的 content 必须是随后发送正文的脱敏版本,仅允许替换凭证和个人信息;执行采集后发送原定回复。禁止先发送后采集、发送后补采或在脱敏之外改写采集内容。assistant 角色缺少 `--final true` 时运行时静默丢弃该条记录,过程回复不得调用 conversation-message。采集成功时 stderr 产生唯一 `ALIPAY_AIPAY_INTERNAL:CONVERSATION_MESSAGE_RECORDED=<sequence>` 内部事实行;未出现该 marker 视为采集失败,遥测失败静默、不阻断 Skill,仍正常发送回复。
47
+ 助手最终收口回复固定采用先采集、后发送:Agent 组织好本轮最终收口回复后,先执行上述 `conversation-message` 并追加 `--final true`,`--message-json` 的 content 必须是随后发送正文的脱敏版本,仅允许替换凭证、个人信息及原流程禁止记录的公钥和临时 URL;执行采集后发送原定回复。禁止先发送后采集、发送后补采或在脱敏之外改写采集内容。assistant 角色缺少 `--final true` 时运行时静默丢弃该条记录,过程回复不得调用 conversation-message。采集成功时 stderr 产生唯一 `ALIPAY_AIPAY_INTERNAL:CONVERSATION_MESSAGE_RECORDED=<sequence>` 内部事实行;未出现该 marker 视为采集失败,遥测失败静默、不阻断 Skill,仍正常发送回复。
48
48
 
49
49
  MESSAGE_ID 可复用用户消息的 USER_INPUT_ID;本地按 conversation_id+role+message_id 的 hash 幂等分配 sequence,消息身份本身不上报。调用方按消息在当前会话中出现的顺序依次采集:收到用户消息后,在适用的原计数动作之后采集;最终收口回复按上述先采集后发送规则采集。不得先采集后出现的消息,再补采前面的消息。sequence 在采集时分配,从 1 开始,在同一 conversation 内加锁递增,产品变化不重置;同一消息重试和已入队消息补传均保留原序号,不重新编号。消费端按 conversation_id+sequence 去重并按 sequence 排序,不依赖网络到达先后。该序号仍表示采集顺序,不能自动纠正漏采后补采的原始位置;不上传原始消息时间。
50
50
 
@@ -88,16 +88,16 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry end-conversat
88
88
 
89
89
  ## 脱敏、本地缓存与共用发送
90
90
 
91
- 调用方仅对凭证和个人信息做语义脱敏,保留字段名、周围说明、代码结构及非敏感参数。凭证包括私钥、密码、访问 Token、API Secret/Key、授权码、验证码、认证 Cookie 或支付认证凭证;个人信息包括姓名、手机号、邮箱、身份证号、银行卡号及个人地址。自由叙述中的姓名、住址等由调用方按语义判断,脚本不能保证覆盖任意表达。
91
+ 调用方对凭证、个人信息及原流程禁止记录的公钥和临时 URL 做语义脱敏,保留字段名、周围说明、代码结构及非敏感参数。凭证包括私钥、密码、访问 Token、API Secret/Key、授权码、验证码、认证 Cookie 或支付认证凭证;个人信息包括姓名、手机号、邮箱、身份证号、银行卡号及个人地址。自由叙述中的姓名、住址等由调用方按语义判断,脚本不能保证覆盖任意表达。
92
92
 
93
- 脚本在落盘和构造请求前再次过滤私钥 PEM、可解析为 PKCS#8/PKCS#1/SEC1 的无头单行 Base64 私钥、Bearer/Basic 认证值、完整 Cookie 值、带前缀的凭证字段、明确标注的个人信息字段,以及可识别的邮箱、手机号和身份证格式。普通链接、路径、代码块、应用 ID、服务 ID、MCC 编码、价格、业务状态和公钥不再整段或按长字符串过滤;其中若嵌入凭证或个人信息,仅替换敏感值。例如带 token 的 URL 保留地址和其他参数,仅遮盖 token 值。格式匹配仍可能误判或漏判,不能声称完全匿名。
93
+ 脚本在落盘和构造请求前再次过滤私钥和公钥 PEM、可解析的无头单行或换行 Base64 密钥、公钥字段、deviceCode/keyConfirmToken 字段、Bearer/Basic 认证值、完整 Cookie 值、带前缀的凭证字段、明确标注的个人信息字段,以及可识别的邮箱、手机号和身份证格式。原流程限制为当次调用使用的应用公钥不得进入采集队列;授权、公钥确认等临时 URL 整条移除,URL 规则复用当前渠道 policy-rules.json 的 temporaryUrlAllowlist,含 deviceCode/keyConfirmToken(含编码后的参数名)的其他 URL 也整条移除。URL 规则不可读时保守移除全部 URL,不阻断启动或自更新。普通帮助链接、路径、代码结构、应用 ID、服务 ID、MCC 编码、价格和业务状态继续保留;其中嵌入的凭证或个人信息仍遮盖敏感值。格式匹配仍可能误判或漏判,不能声称完全匿名。
94
94
 
95
95
  采集脱敏后的完整正文,不主动截断或摘要。
96
96
 
97
- 独立 conversation state 只保存随机 ID、会话 hash、去重 hash、消息序号、创建时授权 ID 和 ACTIVE/CLOSED 状态;正文仅进入当前用户的 outbox。Unix/macOS/Linux 使用 `0600` 文件模式,Windows 仅检查目录实际可写,不声称已经配置 ACL。对话事件使用 .conversation 后缀保存 JSON,让只识别 .json 的旧版 worker 跳过。对话正文和解析错误不进入 stdout/stderr。code 入队时固定,后续改配置不重标旧事件。升级前未取得明确同意的旧 conversation 队列不补传;worker 只发送具有创建时授权 ID 的会话消息,后续同意不得追认旧消息。
97
+ 独立 conversation state 只保存随机 ID、会话 hash、去重 hash、消息序号、待确认消息的序号/去重 hash/时间戳、创建时授权 ID 和 ACTIVE/CLOSED 状态;正文仅进入当前用户的 outbox。Unix/macOS/Linux 使用 `0600` 文件模式,Windows 仅检查目录实际可写,不声称已经配置 ACL。对话事件使用 .conversation 后缀保存 JSON,让只识别 .json 的旧版 worker 跳过。对话正文和解析错误不进入 stdout/stderr。code 入队时固定,后续改配置不重标旧事件。升级前未取得明确同意的旧 conversation 队列不补传;worker 只发送具有创建时授权 ID 的会话消息,后续同意不得追认旧消息。
98
98
 
99
- 对话上报请求不设置脚本级网络超时(timeout=0)。采集命令完成本地状态记录和入队后,尝试启动 detached 后台 worker 并立即返回,不在前台等待网络结果。后台发送也负责失败补传;若网络一直不返回结果,仅后台请求等待,不拖住 Skill 的后续命令。前台 flush-outbox 跳过对话记录,把它们留给后台 worker,避免补传重新引入前台等待。后台 worker 启动失败时保留队列,Skill 继续;后续触发时再尝试启动,不保证最终送达。操作系统、宿主和服务端仍可能中止连接;明确失败时继续按原机制保留记录补传。本地状态写入和入队仍需要少量执行时间,不能把“不等待网络”描述为完全零耗时。
99
+ 对话上报请求最多等待 10 秒(包括 DNS、连接和响应正文等待),每个后台 worker 最多运行 60 秒。同一渠道、同一 outbox 只允许一个后台 worker 执行上传;上传者记录包含开始及失效时间,有效期最多 70 秒(60 秒运行上限及 10 秒退出余量),旧版记录按文件修改时间计算同样的上限。仅在记录有效且进程仍存活时阻止重复启动,过期记录不会因进程号被其他程序复用而永久阻塞;并发启动时由队列锁决定唯一上传者。失效记录对应的 PID 若仍存活,其占用中的记录保留原有 claim TTL 恢复规则,不提前抢占其他活跃发送。每批成功后重新检查队列,继续处理上传期间新增的消息;前台启动上传只尝试取得队列锁,不等待;后台释放退出锁后再检查队列,接手退出窗口内新增的消息。到期前已有成功上传且仍有积压时,释放当前记录后交给新的限时 worker;失败或首条请求持续无响应时不循环拉起新进程。授权保存、采集和结束命令不等待会话锁,锁忙时静默失败并继续业务;过期锁按原规则恢复后只重试一次。成功采集完成本地状态记录和入队即返回,后台启动锁忙时保留队列,不在前台等待锁或网络结果。超时或失败保留未发送记录,worker 到期退出时释放自己占用的记录;异常退出后,后续 worker 检查原进程已结束便恢复其占用记录并重试。前台 flush-outbox 跳过对话记录,避免补传引入前台等待。后台启动失败时保留队列,后续触发时再尝试,不保证最终送达。本地状态写入和入队仍需要少量执行时间。
100
100
 
101
- 除上述对话上报的超时与后台发送规则外,上传、claim 恢复、补传和队列保留/清理规则沿用 [telemetry.md 的“进度与上传”](telemetry.md#进度与上传)。state 成功但 outbox 写入失败时允许丢失且不重建,Skill 继续运行。
101
+ 除上述对话上报的超时与后台发送规则外,上传、claim 恢复、补传和队列保留/清理规则沿用 [telemetry.md 的“进度与上传”](telemetry.md#进度与上传)。对话消息先在会话状态中私有保存待确认序号、去重 hash 与事件时间戳,成功入队后再提交正式序号和去重记录;待确认标记不含正文,不代表采集成功。入队失败不推进正式序号、不输出成功回执,目录恢复后可重试同一消息。状态提交失败时尝试清理本次队列记录。异常退出后,下一次采集或后台上传在同一会话锁内核对待确认标记与队列:已入队则恢复原序号及去重,未入队则清除标记;核对或保存失败时停止本次采集/发送,不占用或重复分配序号。后台仍跳过尚未提交且无法恢复的记录。采集成功仅表示本地入队和状态提交成功,不表示已上传。失败时停止继续传入正文,Skill 业务继续运行。
102
102
 
103
103
  预发版默认遥测根目录为系统临时目录下 `alipay-aipay-telemetry/pre`,与正式版的 run、outbox、上传状态及采集授权隔离;手动设置目录/文件覆盖变量时须自行按渠道分开,不能指向同一状态位置。阶段与对话队列各自保留最多 1000 条及 7 天;对话数量不挤占阶段配额。后台先处理阶段队列,再发送对话;对话写入/启动/上传失败均不改写阶段上传状态,前台联网补传判定只统计阶段队列。对话入队外壳采用 schema_ver=2,固定采集时的 platform,跨宿主补传不改变 d2;旧 schema_ver=1 外壳仍兼容。
@@ -1,4 +1,5 @@
1
- import { createPrivateKey } from 'node:crypto';
1
+ import { createPrivateKey, createPublicKey } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
2
3
 
3
4
  // Conversation payloads have their own schema and code; stage metrics never carry text.
4
5
  const roles = new Set(['user', 'assistant']);
@@ -6,6 +7,7 @@ const fields = new Set(['schema_ver', 'skill', 'skill_version', 'conversation_id
6
7
  'timestamp', 'product_type', 'kind', 'code', 'sequence', 'role',
7
8
  'content', 'has_attachments', 'truncated', 'redaction_version']);
8
9
  const disableValues = new Set(['0', 'false', 'off', 'no']);
10
+ const temporaryCredential = /^(?:device[_-]?code|key[_-]?confirm[_-]?token)$/i;
9
11
 
10
12
  export function conversationTelemetryEnabled(env = process.env) {
11
13
  const value = env.ALIPAY_AIPAY_CONVERSATION_TELEMETRY;
@@ -19,7 +21,7 @@ export function conversationCode(env = process.env) {
19
21
  }
20
22
 
21
23
  // Match explicit credential/PII field names rather than arbitrary IDs or long strings.
22
- const credentialField = String.raw`(?:(?:[a-z][a-z0-9]*[_-])*(?:password|passwd|pwd|(?:api|app|client|access|refresh|auth|id|session)[_-]?(?:key|secret|token)|secret|token|private[_-]?key|authorization|cookie|payment[_-]?proof|signature|auth[_-]?code|verification[_-]?code|(?:应用|支付宝)?私钥|密码|口令|授权码|验证码|访问令牌|访问密钥))`;
24
+ const credentialField = String.raw`(?:(?:[a-z][a-z0-9]*[_-])*(?:password|passwd|pwd|(?:api|app|client|access|refresh|auth|id|session)[_-]?(?:key|secret|token)|secret|token|private[_-]?key|(?:app|alipay|rsa2?)[_-]?public[_-]?key|public[_-]?key|device[_-]?code|key[_-]?confirm[_-]?token|authorization|cookie|payment[_-]?proof|signature|auth[_-]?code|verification[_-]?code|(?:应用|支付宝)?私钥|(?:应用|支付宝|RSA2?)?公钥|密码|口令|授权码|验证码|访问令牌|访问密钥))`;
23
25
  const personalField = String.raw`(?:姓名|联系人|手机号?|电话|邮箱|身份证(?:号)?|银行卡(?:号)?|家庭住址|个人住址|联系地址|收货地址|住址|full[_-]?name|phone(?:[_-]?number)?|mobile|email|id[_-]?card|bank[_-]?account|home[_-]?address)`;
24
26
 
25
27
  function redactFields(content, field, allowSpaces = false) {
@@ -33,31 +35,124 @@ function redactFields(content, field, allowSpaces = false) {
33
35
  });
34
36
  }
35
37
 
36
- // The local key page exports bare Base64 DER, without PEM headers or field labels.
37
- // Parse candidates as private keys so public keys and ordinary long IDs stay intact.
38
- function redactBarePrivateKeys(content) {
39
- return content.replace(/(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{128,}={0,2}(?![A-Za-z0-9+/=])/g, value => {
40
- const bytes = Buffer.from(value, 'base64');
41
- try {
42
- for (const type of ['pkcs8', 'pkcs1', 'sec1']) {
43
- try {
44
- createPrivateKey({ key: bytes, format: 'der', type });
45
- return '[REDACTED]';
46
- } catch {}
38
+ // Recognize key material instead of masking arbitrary long IDs. Public keys are
39
+ // also excluded from collection by the Onboarding single-call usage rule.
40
+ function isEncodedKey(value) {
41
+ if (value.length < 128) return false;
42
+ const bytes = Buffer.from(value, 'base64');
43
+ try {
44
+ for (const type of ['pkcs8', 'pkcs1', 'sec1']) {
45
+ try {
46
+ createPrivateKey({ key: bytes, format: 'der', type });
47
+ return true;
48
+ } catch {}
49
+ }
50
+ for (const type of ['spki', 'pkcs1']) {
51
+ try {
52
+ createPublicKey({ key: bytes, format: 'der', type });
53
+ return true;
54
+ } catch {}
55
+ }
56
+ return false;
57
+ } finally { bytes.fill(0); }
58
+ }
59
+
60
+ // Read only the DER length header, so incomplete/invalid multiline candidates
61
+ // never cause repeated cryptographic parsing of every growing line prefix.
62
+ function encodedKeyCharacters(value) {
63
+ const header = Buffer.from(value.slice(0, 12), 'base64');
64
+ try {
65
+ if (header.length < 2 || header[0] !== 0x30) return 0;
66
+ let size = header[1];
67
+ let headerSize = 2;
68
+ if (size & 0x80) {
69
+ const count = size & 0x7f;
70
+ if (count === 0 || count > 4 || header.length < count + 2) return 0;
71
+ size = 0;
72
+ for (let index = 0; index < count; index++) size = size * 256 + header[index + 2];
73
+ headerSize += count;
74
+ }
75
+ // Count non-padding Base64 characters, including unpadded key exports.
76
+ return size > 0 ? Math.ceil((headerSize + size) * 4 / 3) : 0;
77
+ } finally { header.fill(0); }
78
+ }
79
+
80
+ function redactBareKeys(content) {
81
+ // DER keys start with an ASN.1 sequence (Base64 M[A-P]). Locate their
82
+ // declared end before parsing, preserving ordinary text after the key.
83
+ content = content.replace(/(?<![A-Za-z0-9+/])M[A-P][A-Za-z0-9+/]{14,}(?:[ \t]*\r?\n[ \t]*[A-Za-z0-9+/]+={0,2})+/g, value => {
84
+ const parts = value.split(/([ \t]*\r?\n[ \t]*)/);
85
+ const ends = [];
86
+ let characters = 0;
87
+ for (let index = 0; index < parts.length; index += 2) ends.push(characters += parts[index].length);
88
+ let result = '';
89
+ for (let start = 0; start < parts.length;) {
90
+ const size = /^M[A-P]/.test(parts[start]) ? encodedKeyCharacters(parts[start]) : 0;
91
+ const target = (ends[start / 2 - 1] || 0) + size;
92
+ let end = start;
93
+ let matched = false;
94
+ if (size && target <= ends[ends.length - 1]) {
95
+ let low = start / 2;
96
+ let high = ends.length - 1;
97
+ while (low < high) {
98
+ const middle = Math.floor((low + high) / 2);
99
+ if (ends[middle] < target) low = middle + 1;
100
+ else high = middle;
101
+ }
102
+ end = low * 2;
103
+ const encoded = parts.slice(start, end + 1).filter((_, index) => index % 2 === 0).join('');
104
+ matched = isEncodedKey(encoded);
47
105
  }
48
- return value;
49
- } finally { bytes.fill(0); }
106
+ if (matched) {
107
+ result += '[REDACTED]' + (parts[end + 1] || '');
108
+ start = end + 2;
109
+ } else {
110
+ result += parts[start] + (parts[start + 1] || '');
111
+ start += 2;
112
+ }
113
+ }
114
+ return result;
115
+ });
116
+ return content.replace(/(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{128,}={0,2}(?![A-Za-z0-9+/=])/g,
117
+ value => isEncodedKey(value) ? '[REDACTED]' : value);
118
+ }
119
+
120
+ function redactTemporaryUrls(content) {
121
+ // Load business URL rules only during sanitization, never during startup or
122
+ // self-update. An unreadable policy conservatively excludes all URLs.
123
+ let rules;
124
+ try {
125
+ rules = Object.values(JSON.parse(readFileSync(
126
+ new URL('../json/policy-rules.json', import.meta.url), 'utf8'
127
+ )).temporaryUrlAllowlist).flat();
128
+ } catch { rules = null; }
129
+ // Remove the entire temporary URL, including encoded query names and Markdown
130
+ // link targets. Ordinary help links remain useful in the collected dialogue.
131
+ return content.replace(/https?:\\?\/\\?\/[^\s<>"'`,。;!?、))\]]+/giu, raw => {
132
+ if (!rules) return '[REDACTED]';
133
+ try {
134
+ const url = new URL(raw.replace(/\\\//g, '/'));
135
+ const credentials = [...url.searchParams.keys(), ...new URLSearchParams(url.hash.slice(1)).keys()];
136
+ if (credentials.some(name => temporaryCredential.test(name))) return '[REDACTED]';
137
+ const temporary = rules.some(rule => url.hostname === rule.host && url.pathname === rule.path
138
+ && (rule.path !== '/' || rule.requiredQueryParams.every(name => url.searchParams.has(name))));
139
+ if (temporary) return '[REDACTED]';
140
+ } catch {
141
+ if (/(?:device[_-]?code|key[_-]?confirm[_-]?token)=/i.test(raw)) return '[REDACTED]';
142
+ }
143
+ return raw;
50
144
  });
51
145
  }
52
146
 
53
147
  export function sanitizeConversationText(value) {
54
148
  if (typeof value !== 'string') return null;
55
149
  // The caller handles personal facts in free prose semantically. This second pass
56
- // preserves public keys, IDs, code, paths and URLs except credential/PII values.
150
+ // excludes keys and temporary URLs, while retaining ordinary code, IDs and help links.
57
151
  let content = value
58
- .replace(/-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----[\s\S]*?(?:-----END (?:[A-Z]+ )*PRIVATE KEY-----|$)/g, '[REDACTED]')
152
+ .replace(/-----BEGIN (?:[A-Z]+ )*(?:PRIVATE|PUBLIC) KEY-----[\s\S]*?(?:-----END (?:[A-Z]+ )*(?:PRIVATE|PUBLIC) KEY-----|$)/g, '[REDACTED]')
59
153
  .replace(/\b(Bearer|Basic)([ \t]+)(?!\[REDACTED\])[^\s"'`<>,;]+/gi, '$1$2[REDACTED]');
60
- content = redactBarePrivateKeys(content);
154
+ content = redactBareKeys(content);
155
+ content = redactTemporaryUrls(content);
61
156
  // An unquoted Cookie header contains multiple semicolon-separated credentials.
62
157
  content = content.replace(/(?<![\w.-])((?:cookie|set-cookie)[\"']?\s*[::=]\s*)(?![\"'])([^\s\"'`<>][^\r\n\"'`<>]*)/gi, '$1[REDACTED]');
63
158
  content = redactFields(content, credentialField);
@@ -1,9 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync as nativeSpawn } from 'node:child_process';
4
- const { spawnSync } = process.platform === 'win32'
5
- ? await import('./windows/execution.mjs')
6
- : { spawnSync: nativeSpawn };
7
4
  import path from 'node:path';
8
5
  import process from 'node:process';
9
6
  import { fileURLToPath } from 'node:url';
@@ -69,7 +66,10 @@ function cliArgv(args) {
69
66
  return [process.execPath, fileURLToPath(import.meta.url), ...args];
70
67
  }
71
68
 
72
- function runShell(script, cwd, args) {
69
+ async function runShell(script, cwd, args) {
70
+ const spawnSync = process.platform === 'win32'
71
+ ? (await import('./windows/execution.mjs')).spawnSync
72
+ : nativeSpawn;
73
73
  const captureWindowsSandbox = process.platform === 'win32' && path.basename(script) === 'sandbox_config.sh';
74
74
  const result = spawnSync('bash', [script, ...args], {
75
75
  cwd,
@@ -307,17 +307,14 @@ function removeTransactionPath(target) {
307
307
 
308
308
  function reconcileInstallTransaction(skillDir, transactionId, channel) {
309
309
  const artifacts = transactionArtifacts(skillDir, transactionId, channel);
310
- let current = readLocalVersion(skillDir);
311
- if (!current && isRecoverableSkillRoot(artifacts.backup)) {
310
+ if (isRecoverableSkillRoot(artifacts.backup)) {
312
311
  try {
313
312
  removeTransactionPath(skillDir);
314
313
  fs.renameSync(artifacts.backup, skillDir);
315
314
  } catch {}
316
- current = readLocalVersion(skillDir);
317
315
  }
318
316
  removeTransactionPath(artifacts.staging);
319
- if (isRecoverableSkillRoot(skillDir)) removeTransactionPath(artifacts.backup);
320
- return current;
317
+ return { version: readLocalVersion(skillDir), backupPresent: fs.existsSync(artifacts.backup) };
321
318
  }
322
319
 
323
320
  async function installVersion(skillDir, packageVersion, channel) {
@@ -351,10 +348,11 @@ async function installVersion(skillDir, packageVersion, channel) {
351
348
  } catch { /* An invalid receipt never permits a session switch. */ }
352
349
  }
353
350
  }
351
+ const reconciled = reconcileInstallTransaction(skillDir, transactionId, channel);
354
352
  return {
355
353
  ok: result.status === 0 && !result.error,
356
354
  reason: result.status === 0 && !result.error ? 'ok' : packageManagerFailureKind(result),
357
- version: reconcileInstallTransaction(skillDir, transactionId, channel),
355
+ ...reconciled,
358
356
  session,
359
357
  };
360
358
  }
@@ -395,8 +393,9 @@ export async function runSelfUpdateCli(argv = process.argv) {
395
393
  if (installed.session && installed.session.version===latest.raw) {
396
394
  return {kind:'SESSION_READY',version:latest,skillDir:installed.session.skillDir};
397
395
  }
396
+ if (installed.backupPresent) return { kind: 'INSTALL_FAILED' };
398
397
  const updated = installed.version || readLocalVersion(skillDir);
399
- if (updated && readChannel(skillDir) === channel && compareVersions(updated, latest) >= 0) return { kind: 'UPDATED', version: updated };
398
+ if (installed.ok && updated && readChannel(skillDir) === channel && compareVersions(updated, latest) >= 0) return { kind: 'UPDATED', version: updated };
400
399
  if (installed.reason === 'network') return { kind: 'RETRY_WITH_NETWORK' };
401
400
  if (installed.reason === 'local') return { kind: 'CHECK_FAILED' };
402
401
  return { kind: installed.ok ? 'VERIFY_FAILED' : 'INSTALL_FAILED' };