@shgroup/dsh-serenity-hooks 1.30.8 → 1.30.9
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.
- package/dsh.plugin.json +1 -1
- package/lib/config-ops.d.ts +10 -0
- package/lib/index.js +615 -124
- package/lib/msm-ops.d.ts +1 -1
- package/lib/skiff-core.d.ts +10 -2
- package/lib/{skiff-debug-8pF60JBO.js → skiff-debug-TONJlpgF.js} +23 -4
- package/lib/{weixin-api-OUCuw0Ws.js → weixin-api-BMpljodH.js} +25 -3
- package/lib/weixin-api.d.ts +12 -1
- package/lib/weixin-bridge.d.ts +43 -1
- package/lib/weixin-hook.d.ts +9 -0
- package/lib/weixin-send-api.d.ts +87 -0
- package/lib/weixin-send-endpoint.d.ts +17 -0
- package/package.json +1 -1
package/lib/msm-ops.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const MSM_ACTIONS: readonly MsmAction[];
|
|
|
19
19
|
export declare const ACC_CATALOG = "\u2550\u2550\u2550 ACC Usage Catalog \u2550\u2550\u2550\nDirectory of ACC capabilities \u2014 each area lists where to go for details (guides live with their feature; this catalog only points).\n\n \u2460 \u4F1A\u8BDD\u4E0E\u8F68\u8FF9 \u2192 logbook\uFF08list/show/create/use/close/health/qa/archive/summary/hook-develop-guide + rebuild \u8D85\u9650\u91CD\u5EFA\uFF09\n session_rebuild \u5DF2\u5E76\u5165 logbook rebuild\uFF08\u8D85\u9650\u91CD\u5EFA\uFF1B\u9608\u503C K \u89C1 container_admin ccc-config / \u8BBE\u7F6E\u9762\u677F\u91CD\u5EFA\u9608\u503CK\uFF09\n \u2461 \u8BA4\u77E5\u8D28\u91CF\u6846\u67B6 \u2192 praxis\uFF08eap/neat/cce \u4E09\u5408\u4E00\u2014\u2014\u53EF\u5B9E\u8DF5\u7406\u8BBA\u6CE8\u5165\uFF0Csection \u6E10\u8FDB\u62AB\u9732\uFF0C\u65E0\u53C2\u5373\u76EE\u5F55\uFF09\n \u2462 \u5DE5\u5177\u4E0E\u6267\u884C \u2192 container_fs\uFF08\u5BB9\u5668\u6587\u4EF6\u7CFB\u7EDF 15 \u5B50\u547D\u4EE4\uFF09/ container_git\uFF08git\uFF09/ dashboard\uFF08health \u542B\u6CE8\u518C\u8868\u68C0\u67E5/time/wait\uFF09\n acc_msm \u5DF2\u5E76\u5165 msm\uFF08\u6267\u884C+\u53D1\u73B0\uFF09\u4E0E container_admin\uFF08\u7BA1\u7406\uFF09\u2014\u2014msm(\"<name>\", [\"<args>\"])\uFF1Binspect=true \u67E5\u7528\u6CD5\n handyman\uFF08\u6742\u5DE5 agent \u7F16\u6392\u2014\u2014guide \u5B50\u547D\u4EE4\uFF1Ahandyman guide\uFF09\n \u2463 \u89D2\u8272\u4E0E\u5BF9\u5916\u9762 \u2192 container_admin role\uFF08Skiff \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF1Aguide/validate/apply/list\u2014\u2014skiff_admin \u5DF2\u5E76\u5165\uFF09\n ACP\uFF08\u7A0B\u5E8F\u5316 JSON-RPC 3100\uFF09/ Skiff \u95EE\u7B54\u9875\uFF08\u516C\u7F51 ask\uFF09\u2014\u2014\u9762\u677F\u300C\u5916\u90E8\u80FD\u529B\u300D\u7EC4\n \u2464 \u81EA\u4E3B\u4E0E\u63A5\u5165 \u2192 autopilot-trajectory\uFF08Autopilot \u4E00\u7AD9\u5F0F\uFF1Aall/init/random/diag/doc/check/status/guide\uFF09\n weixin \u5FAE\u4FE1\u6865\uFF08\u914D\u7F6E/\u626B\u7801/\u8DEF\u7531/\u6D88\u606F hook\u2014\u2014CCC \u4FA7 weixin-doctor MSM\uFF1Amsm(\"weixin-doctor\", [\"guide\"])\uFF09\n \u2465 CCC \u914D\u7F6E\u603B\u89C8 \u2192 container_admin msm ccc-config\uFF088 \u6BB5\uFF1Ahandyman/sessionKeeper/localstore/hooks/safeMode/skiff/autopilotTrajectory/weixin\uFF09\n \u2466 \u6CE8\u518C\u8868\u4E0E\u5B89\u5168 \u2192 mech-registry.json \u7531 container_admin msm register/deregister \u7BA1\u7406\uFF08\u5199\u4FDD\u62A4\u2014\u2014\u4E0D\u53EF\u76F4\u63A5\u7F16\u8F91\uFF09\n dashboard health \u8F93\u51FA registry \u5B8C\u6574\u6027\u68C0\u67E5\uFF1B\u635F\u574F\u6062\u590D\u6307\u5F15\u89C1 health registry.issues\n\n\u6BCF\u533A\uFF1A\u4E00\u53E5\u8BDD\u5B9A\u4F4D + \u8BE6\u7EC6\u5165\u53E3\uFF08\u5DE5\u5177\u540D / guide \u5B50\u547D\u4EE4\uFF09\u3002\u8BE6\u60C5\u6C38\u8FDC\u4EE5\u5BF9\u5E94\u5DE5\u5177\u7684 guide/ccc-config \u4E3A\u5355\u4E00\u771F\u76F8\u6E90\u3002\n";
|
|
20
20
|
export declare const MSM_GUIDE = "MSM Development Manual (Mech & Semi-Mech framework)\n\n## What it is\nMSM = the executable-unit layer. Mech is pure TS with zero LLM reasoning; Semi-Mech is a TS framework + LLM decision points.\nMSM is ACC's deterministic executable-unit layer \u2014 all shell/exec operations go through MSM and cannot be bypassed.\n\n## Registering a new MSM (container_admin msm register)\n1. Write the script under <skill>/scripts/ (runnable via tsx; must include a main() CLI guard with an import.meta.url check)\n2. container_admin msm register --domain msm --action register <name> --skill <s> --path <script path relative to root> --category <mech|semi-mech> --description <desc>\n3. Auto-writes mech-registry.json (preserving the original format) + git commit (commits only the registry file)\n4. Validation: path must be inside root, script must exist, name must be globally unique\n\n## flag schema (v1)\nflags is a new-style object array for parameter validation and path-escape guarding:\n [{\"name\":\"output\",\"type\":\"string\",\"description\":\"output path\"},\n {\"name\":\"target\",\"type\":\"path\",\"description\":\"operation target (type:path enables in-root validation + symlink defense)\"},\n {\"name\":\"force\",\"type\":\"boolean\",\"description\":\"force mode\",\"default\":false}]\n- {name, type} format \u2014 new style, type:\"path\" enables the path-escape guard\n- {flag, description} format \u2014 old style, CLI flag description string\n- On registration, flags are passed via container_admin msm register --flags '<json>'\n\n## Script conventions\n- Top-of-file documentation: purpose / usage / exit codes\n- Exit codes: 0 success / 1 user / 2 system / 3 operator (per ACC protocol classification)\n- main() CLI guard (DC-M2): the script must start with\n if (import.meta.url === `file://${process.argv[1]}`) { main() }\n or an equivalent isMain / require.main === check \u2014 so vitest imports never trigger top-level code\n- A paired .test.ts or .spec.ts (DC-M1, vitest)\n- Business subprocess environment: SERENITY_ROOT / SERENITY_CCC / SERENITY_VERSION injected\n\n## Interaction & confirmation conventions (no blocking confirmation)\n- MSMs run in **user-less subprocesses** (spawn/execFile, 600s timeout) \u2014 **never** use\n readline / prompt / process.stdin etc. to block waiting for user input (it would hang until killed by the timeout)\n- When a second confirmation is needed: **directly return the confirmation request** (list the operation to perform and its impact + how to retry with the confirmation flag),\n returning a non-zero exit code (or an explicit hint)\n- After the agent confirms, **re-invoke the MSM with the confirmation flag** (e.g. --confirm / --yes / --force) to retry\n- Standard mode (two-phase):\n 1. First call: destructive/confirmation-requiring operation detected and no confirmation flag present \u2192 output the confirmation request (list operation/impact/rollback),\n exit non-zero (e.g. 1 user), **perform no changes**\n 2. After the agent evaluates, re-invoke: with the confirmation flag \u2192 execute and output the result\n- Applies to: delete/overwrite/push/batch operations and other irreversible or wide-impact operations\n- Anti-example (forbidden): readline waiting for user input, process.stdin.on('data') blocking waits\n (MSMs have no stdin interaction channel \u2014 they hang until the 600s timeout)\n\n## Quality checks (container_admin msm check, DC-M1~M4)\nDC-M1 has .test.ts/.spec.ts; DC-M2 has a main() guard (function main( / isMain / require.main === / import.meta.url);\nDC-M3 bidirectional: scripts unregistered + registry references missing scripts; DC-M4 path-type flags marked type:\"path\"\n\n## Self-description (protocol flags, first argument only)\nmsm(\"<name>\", [\"--list\"]) \u2014 list all MSMs\nmsm(\"<name>\", [\"--schema\", \"<n>\"]) \u2014 view a specific MSM's parameter schema\nmsm(\"<name>\", [\"--format=json\"]) \u2014 JSON output mode (remaining args passed through losslessly)\n";
|
|
21
21
|
/** CCC 配置参考(对齐 osp ccc-config action) */
|
|
22
|
-
export declare const CCC_CONFIG_REFERENCE = "\u2550\u2550\u2550 CCC Configuration Reference \u2550\u2550\u2550\n\nCCC-level features are configured in .opencode/serenity.json.\nBelow are all available configuration sections.\n\n\u2500\u2500 1. handyman.models \u2500\u2500\nhandyman\uFF08\u6742\u5DE5\uFF09\u5DE5\u5177\u53EF\u7528\u6A21\u578B\u767D\u540D\u5355\uFF08provider/model \u5217\u8868\uFF09\uFF1B\u672A\u914D\u7F6E\u65F6 handyman \u62A5\u9519\u8981\u6C42\u914D\u7F6E\u3002\n\u7F3A\u7701\u6A21\u578B = models[0]\uFF08\u53EF\u7528 handyman.defaultModel \u6307\u5B9A\uFF0C\u5FC5\u987B \u2208 models\uFF09\u3002\n\n Config:\n { \"handyman\": { \"models\": [\"provider/model-name\"] } }\n\n Example:\n { \"handyman\": { \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\", \"maxRounds\": 100, \"maxParallel\": 10 } }\n\n\u2500\u2500 2. sessionKeeper.threshold \u2500\u2500\ntrajectory-assistant\uFF08TRAJECTORY-ASSISTANT \u00B7 CHECKPOINT\uFF09\u63D0\u9192\u673A\u5236\u7684\u79EF\u5206\u9608\u503C\uFF08\u975E headless \u4E3B agent\uFF09\u3002\n\u6309\u5DE5\u5177\u8C03\u7528\u52A0\u6743 + \u8017\u65F6\u8BA1\u5206\uFF1B\u8FBE\u5230\u9608\u503C\u6CE8\u5165\u63D0\u9192\uFF0C\u8981\u6C42\u6A21\u578B\u56DE\u590D ACK \u7801\u3002\n\n Config:\n { \"sessionKeeper\": { \"threshold\": 150 } }\n\n \u8BA1\u5206\uFF1Awrite/edit = 3\uFF0Ctask = 10\uFF0Cread/grep/glob/msm \u7B49 = 1\uFF0C\u65F6\u95F4 = 1/\u5206\u949F\n \u9ED8\u8BA4\uFF1A150\n\n\u2500\u2500 3. localstore.gitTrack \u2500\u2500\nlocalstore.json \u7684 git \u7B56\u7565\uFF1Aallow \u53EF\u63D0\u4EA4 / deny \u7981\u63D0\u4EA4\uFF08\u9ED8\u8BA4 deny\uFF09\u3002\ndeny \u65F6\u5199\u5165\u81EA\u52A8\u786E\u4FDD .gitignore \u542B\u8BE5\u6587\u4EF6\uFF08\u7269\u7406\u4FDD\u8BC1\uFF09\uFF0Ccontainer_git commit \u4F1A\u68C0\u67E5\u62D2\u7EDD\u3002\n\n Config:\n { \"localstore\": { \"gitTrack\": \"allow\" } }\n\n\u2500\u2500 4. hooks.autoRestoreSession \u2500\u2500\n\u4F1A\u8BDD\u81EA\u52A8\u6062\u590D\uFF08\u9ED8\u8BA4 true\uFF1B\u53D7 events \u95E8\u63A7\u2014\u2014\u4EC5\u6839\u4F1A\u8BDD + \u5DF2\u6709\u5BF9\u8BDD\u5386\u53F2\u624D\u6062\u590D\uFF09\u3002\n\n Config:\n { \"hooks\": { \"autoRestoreSession\": false } }\n\n\u2500\u2500 5. safeMode / blacklist \u2500\u2500\nsafe-mode \u7531 WebUI \u5F00\u5173\u63A7\u5236\uFF08\u5199 .serenity-safe-on \u6807\u8BB0\uFF09\uFF1B\u9ED1\u540D\u5355\u8DEF\u5F84\u53D7 guards seam \u62E6\u622A\u3002\n\n Config:\n { \"safeMode\": { \"blacklist\": [\".secrets/\"] } }\n\n\u2500\u2500 6. skiff.roles\uFF08F4 \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF09\u2500\u2500\nSkiff \u89D2\u8272 = \u5168\u77E5\u5168\u80FD trajectory \u7684\u4EFB\u610F\u5B50\u96C6\uFF08CCC \u5B9A\u4E49\uFF09\u3002\u6BCF\u89D2\u8272\u72EC\u7ACB\u6A21\u578B + \u53CC\u767D\u540D\u5355\n\uFF08MSM \u767D\u540D\u5355 msms[] \u4E0E\u975E MSM \u5DE5\u5177\u767D\u540D\u5355 tools[]\uFF0C\u767D\u540D\u5355\u5916\u5168\u9690\u85CF\uFF1Bskill \u52A0\u8F7D\u6052\u53EF\u7528\uFF09\u3002\ntrajectory \u7EAA\u5F8B\u5B50\u96C6\uFF08session/keeper/rebuild \u53C2\u4E0E\u9879\uFF09\u9ED8\u8BA4\u5168\u5173 = \u5B8C\u5168\u72EC\u7ACB\u3002\nsystemPrompt \u5185\u8054 \u6216 systemPromptFile\uFF08.md \u5F15\u7528\uFF0C\u63A8\u8350\uFF09\u2014\u2014\u89D2\u8272\u4F1A\u8BDD\u7684\u5B8C\u6574\u4EBA\u683C/\u8FB9\u754C\u63D0\u793A\u8BCD\u3002\nvalidate \u6821\u9A8C / apply \u751F\u6548 / list \u67E5\u770B\uFF1Acontainer_admin role <guide|validate|apply|list>\uFF08skiff_admin \u5DF2\u5E76\u5165\uFF09\u3002\n\n Config:\n { \"skiff\": { \"roles\": {\n \"qa\": {\n \"model\": \"provider/model\",\n \"msms\": [\"web-search\", \"vlm-describe\"],\n \"tools\": [\"read\", \"grep\", \"glob\"],\n \"systemPromptFile\": \".opencode/skiff/qa.md\"\n } } } }\n\n\u2500\u2500 7. autopilotTrajectory\uFF08\u81EA\u52A8\u5DE1\u822A\u8F68\u8FF9\uFF09\u2500\u2500\nCCC \u5B9A\u4E49\u7684\u4E00\u6761\u81EA\u4E3B trajectory\u2014\u2014\u65F6\u949F\u5230\u70B9\u81EA\u52A8\u5524\u8D77\uFF08\u524D\u53F0\u6CE8\u5165\uFF0C\u7528\u6237\u53EF\u89C1\u53EF\u4ECB\u5165\uFF09\u3002\n\u672A\u914D\u7F6E\u6216 enabled=false \u2192 \u5B8C\u5168\u4E0D\u542F\u52A8\uFF08\u96F6\u8D44\u6E90\u5360\u7528\uFF09\u3002\u591A CCC \u72EC\u7ACB\uFF1A\u6BCF CCC \u81EA\u5DF1\u7684\u914D\u7F6E\u3002\n\u5524\u8D77\u6D88\u606F\u56DB\u6BB5\u5F0F\uFF1A\u8F68\u8FF9\u7126\u70B9 topPrompt\uFF08\u6700\u5148\u6CE8\u5165\uFF0C\u7A33\u5B9A\u951A\uFF09\u2192 \u8EAB\u4EFD\u951A\u5B9A \u2192 \u5148\u9A8C\u504F\u89C1\n\uFF08CCC \u6839\u811A\u672C biasProvider \u8F93\u51FA\uFF09\u2192 \u4EFB\u52A1\u3002\u76EE\u6807\u4F1A\u8BDD session \u5FC5\u586B\uFF08\u76EE\u5F55\u987B\u5E26 --auto \u540E\u7F00\uFF09\u3002\n\u8BCA\u65AD/\u72B6\u6001/\u7ACB\u5373\u5524\u8D77\uFF1Aautopilot-trajectory <all|check|status|diag>\uFF08msm(\"autopilot-trajectory\", [\"all\"])\uFF09\u3002\n\n Config:\n { \"autopilotTrajectory\": {\n \"enabled\": true,\n \"intervalHours\": 2,\n \"session\": \"S151\",\n \"biasProvider\": \"autopilot-bias.ts\",\n \"topPrompt\": \"\u672C\u8F68\u8FF9\u6838\u5FC3\u76EE\u6807/\u7EAA\u5F8B/\u8D28\u91CF\u8981\u6C42\uFF08CCC \u81EA\u586B\uFF0C\u9632\u6F02\u79FB\uFF09\",\n \"avoidWakeHours\": { \"start\": 8, \"end\": 18 }\n } }\n\n\u2500\u2500 8. weixin\uFF08\u5FAE\u4FE1\u6865 F4c-3\uFF0C\u542B\u6D88\u606F\u8BB0\u5F55 hook\uFF09\u2500\u2500\nCCC \u7EA7\u5FAE\u4FE1\u4E2A\u4EBA\u53F7\u63A5\u5165\uFF08iLink \u534F\u8BAE\uFF09\uFF1Adsh \u4E00\u8FDB\u7A0B\u591A CCC\uFF0C\u6BCF CCC \u72EC\u7ACB\u5BF9\u63A5\u5FAE\u4FE1\u6865\u3002\n\u8D26\u53F7/\u8DEF\u7531/\u5F00\u5173\u5728\u6B64\u6587\u4EF6\uFF1B**bot_token \u51ED\u636E\u5728 CCC localstore credential scope**\n\uFF08\u626B\u7801\u7ED1\u5B9A\u540E\u81EA\u52A8\u5199\u5165\uFF0C\u6C38\u4E0D\u8FDB git \u660E\u6587\u9762\uFF09\u3002\n\u8DEF\u7531 user \u2192 role\uFF1Aexact \u4F18\u5148\uFF0C* \u901A\u914D\u515C\u5E95\uFF1Brole \u5FC5\u987B \u2208 \u8BE5 CCC skiff.roles\u3002\n\u9762\u677F\uFF08WebUI \u8BBE\u7F6E \u2192 \u5FAE\u4FE1\u6865\uFF09\u53EF\u626B\u7801\u7ED1\u5B9A/\u79FB\u9664\u8D26\u53F7/\u7F16\u8F91\u8DEF\u7531\uFF1Bmsm(\"weixin-doctor\", [\"status\"|\"diag\"|\"verify\"]) \u6392\u67E5\u3002\u51ED\u636E\u4E3B\u52A8\u67E5\u770B\uFF1Alocalstore get WEIXIN_<ACCOUNT>_TOKEN\u3002\n\n Config:\n { \"weixin\": {\n \"enabled\": true,\n \"hook\": \"scripts/weixin-message-hook.ts\",\n \"accounts\": [{ \"accountId\": \"wechat-1\", \"name\": \"\u5BB6\u5EAD\u52A9\u624B\", \"enabled\": true }],\n \"routes\": [{ \"user\": \"*\", \"role\": \"zhaocai\" }]\n } }\n\n \u25B8 weixin.hook\uFF08\u6D88\u606F\u8BB0\u5F55 hook\uFF0Cv1.27.13\uFF09\uFF1A\n \u5FAE\u4FE1\u6865\u6BCF\u6536/\u53D1\u4E00\u6761\u6D88\u606F\u89E6\u53D1\u4E00\u6B21 CCC \u81EA\u5199\u811A\u672C\uFF0C\u7531 CCC \u81EA\u884C\u6301\u4E45\u5316\u4FDD\u5B58\uFF08\u5B58\u54EA/\u5B58\u6210\u4EC0\u4E48\n /\u662F\u5426\u5165\u5E93\u5168\u5F52 CCC\u2014\u2014ACC \u4E0D\u7ED1\u5B9A\u5B58\u50A8\uFF09\u3002\u672A\u914D\u7F6E hook \u2192 \u96F6\u53D8\u5316\uFF08\u4E0D\u89E6\u53D1\uFF09\u3002\n\n \u89E6\u53D1\uFF08\u53CC\u5411\uFF09\uFF1A\n incoming = \u7528\u6237 \u2192 bot\uFF1A\u8DEF\u7531\u547D\u4E2D\u540E\u3001\u5A92\u4F53\u843D\u76D8\u540E\u89E6\u53D1\uFF08\u6587\u672C\u542B\u8BED\u97F3\u8F6C\u5199\uFF1B\u5A92\u4F53\u5E26\u843D\u76D8 relPath\uFF09\n outgoing = bot \u2192 \u7528\u6237\uFF1A\u56DE\u590D\u53D1\u9001\u6210\u529F\u540E\u89E6\u53D1\uFF08reply = \u7528\u6237\u5B9E\u9645\u6536\u5230\u7684\u7EAF\u6587\u672C\uFF0C\u5DF2\u5265\u79BB think\uFF09\n\n \u811A\u672C\u7EA6\u5B9A\uFF08\u4E8B\u4EF6 JSON \u5355\u884C\u7ECF stdin \u4F20\u5165\uFF1Bbun \u4F18\u5148 node \u515C\u5E95\uFF09\uFF1A\n #!/usr/bin/env bun \u6216 node script.js \u2014\u2014 \u8BFB process.stdin \u6574\u6BB5 JSON.parse\n const ev = JSON.parse(await new Response(process.stdin).text())\n ev.event === 'incoming' | 'outgoing'\n\n \u4E8B\u4EF6 schema\uFF08**\u4E0D\u542B\u4EFB\u4F55\u4F1A\u8BDD\u51ED\u636E**\u2014\u2014context_token/bot_token/token/aes_key \u5747\u4E0D\u51FA\u73B0\uFF09\uFF1A\n { \"event\": \"incoming\", \"ts\": 1788359941488, \"cccRoot\": \"/path/ccc\",\n \"accountId\": \"wechat-1\", \"userId\": \"u1@im.wechat\",\n \"sessionId\": \"skiff-weixin-xxx\", \"role\": \"zhaocai\",\n \"message\": { \"text\": \"\u4F60\u597D\", \"media\": [{ \"kind\": \"image\", \"relPath\": \"_tmp/weixin-inbound/<hash>/img_x.jpg\" }] } }\n { \"event\": \"outgoing\", \"ts\": ..., \"cccRoot\": ..., \"accountId\": ...,\n \"userId\": ..., \"sessionId\": ..., \"role\": ...,\n \"reply\": \"\u5DF2\u8BB0\u5F55\uFF08\u7EAF\u6587\u672C\uFF09\" }\n\n \u793A\u4F8B\u811A\u672C\uFF08\u8FFD\u52A0\u5230\u6309\u65E5\u6587\u4EF6\u2014\u2014\u6301\u4E45\u5316\u5F52 CCC \u81EA\u9009\uFF1A\u6587\u4EF6/DB/\u8FDC\u7AEF\u5747\u53EF\uFF09\uFF1A\n const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';\n fs.appendFileSync(p, JSON.stringify(ev) + '\\n');\n\n \u6267\u884C\u8BED\u4E49\uFF08\u65C1\u8DEF\u5BB9\u5FCD\uFF09\uFF1A\u5F02\u6B65 fire-and-forget + 15s \u8D85\u65F6 kill + \u5931\u8D25\u4EC5\u65E5\u5FD7\u2014\u2014\n \u5FAE\u4FE1\u6865\u6D88\u606F\u5904\u7406/\u56DE\u590D\u4E0D\u53D7 hook \u5F71\u54CD\uFF1B\u811A\u672C\u987B\u5728 CCC \u6839\u5185\uFF08\u8DEF\u5F84\u9003\u9038\u62D2\u7EDD\uFF09\u3002\n \u5A92\u4F53 relPath \u6307\u5411 _tmp/weixin-inbound/\uFF08\u4E34\u65F6\u76EE\u5F55\uFF09\u2014\u2014\u9700\u6301\u4E45\u4FDD\u5B58\u5A92\u4F53\u6587\u4EF6\u8BF7\u81EA\u884C copy\u3002\n";
|
|
22
|
+
export declare const CCC_CONFIG_REFERENCE = "\u2550\u2550\u2550 CCC Configuration Reference \u2550\u2550\u2550\n\nCCC-level features are configured in .opencode/serenity.json.\nBelow are all available configuration sections.\n\n\u2500\u2500 1. handyman.models \u2500\u2500\nhandyman\uFF08\u6742\u5DE5\uFF09\u5DE5\u5177\u53EF\u7528\u6A21\u578B\u767D\u540D\u5355\uFF08provider/model \u5217\u8868\uFF09\uFF1B\u672A\u914D\u7F6E\u65F6 handyman \u62A5\u9519\u8981\u6C42\u914D\u7F6E\u3002\n\u7F3A\u7701\u6A21\u578B = models[0]\uFF08\u53EF\u7528 handyman.defaultModel \u6307\u5B9A\uFF0C\u5FC5\u987B \u2208 models\uFF09\u3002\n\n Config:\n { \"handyman\": { \"models\": [\"provider/model-name\"] } }\n\n Example:\n { \"handyman\": { \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\", \"maxRounds\": 100, \"maxParallel\": 10 } }\n\n\u2500\u2500 2. sessionKeeper.threshold \u2500\u2500\ntrajectory-assistant\uFF08TRAJECTORY-ASSISTANT \u00B7 CHECKPOINT\uFF09\u63D0\u9192\u673A\u5236\u7684\u79EF\u5206\u9608\u503C\uFF08\u975E headless \u4E3B agent\uFF09\u3002\n\u6309\u5DE5\u5177\u8C03\u7528\u52A0\u6743 + \u8017\u65F6\u8BA1\u5206\uFF1B\u8FBE\u5230\u9608\u503C\u6CE8\u5165\u63D0\u9192\uFF0C\u8981\u6C42\u6A21\u578B\u56DE\u590D ACK \u7801\u3002\n\n Config:\n { \"sessionKeeper\": { \"threshold\": 150 } }\n\n \u8BA1\u5206\uFF1Awrite/edit = 3\uFF0Ctask = 10\uFF0Cread/grep/glob/msm \u7B49 = 1\uFF0C\u65F6\u95F4 = 1/\u5206\u949F\n \u9ED8\u8BA4\uFF1A150\n\n\u2500\u2500 3. localstore.gitTrack \u2500\u2500\nlocalstore.json \u7684 git \u7B56\u7565\uFF1Aallow \u53EF\u63D0\u4EA4 / deny \u7981\u63D0\u4EA4\uFF08\u9ED8\u8BA4 deny\uFF09\u3002\ndeny \u65F6\u5199\u5165\u81EA\u52A8\u786E\u4FDD .gitignore \u542B\u8BE5\u6587\u4EF6\uFF08\u7269\u7406\u4FDD\u8BC1\uFF09\uFF0Ccontainer_git commit \u4F1A\u68C0\u67E5\u62D2\u7EDD\u3002\n\n Config:\n { \"localstore\": { \"gitTrack\": \"allow\" } }\n\n\u2500\u2500 4. hooks.autoRestoreSession \u2500\u2500\n\u4F1A\u8BDD\u81EA\u52A8\u6062\u590D\uFF08\u9ED8\u8BA4 true\uFF1B\u53D7 events \u95E8\u63A7\u2014\u2014\u4EC5\u6839\u4F1A\u8BDD + \u5DF2\u6709\u5BF9\u8BDD\u5386\u53F2\u624D\u6062\u590D\uFF09\u3002\n\n Config:\n { \"hooks\": { \"autoRestoreSession\": false } }\n\n\u2500\u2500 5. safeMode / blacklist \u2500\u2500\nsafe-mode \u7531 WebUI \u5F00\u5173\u63A7\u5236\uFF08\u5199 .serenity-safe-on \u6807\u8BB0\uFF09\uFF1B\u9ED1\u540D\u5355\u8DEF\u5F84\u53D7 guards seam \u62E6\u622A\u3002\n\n Config:\n { \"safeMode\": { \"blacklist\": [\".secrets/\"] } }\n\n\u2500\u2500 6. skiff.roles\uFF08F4 \u8BA4\u77E5\u5B50\u96C6\u89D2\u8272\uFF09\u2500\u2500\nSkiff \u89D2\u8272 = \u5168\u77E5\u5168\u80FD trajectory \u7684\u4EFB\u610F\u5B50\u96C6\uFF08CCC \u5B9A\u4E49\uFF09\u3002\u6BCF\u89D2\u8272\u72EC\u7ACB\u6A21\u578B + \u53CC\u767D\u540D\u5355\n\uFF08MSM \u767D\u540D\u5355 msms[] \u4E0E\u975E MSM \u5DE5\u5177\u767D\u540D\u5355 tools[]\uFF0C\u767D\u540D\u5355\u5916\u5168\u9690\u85CF\uFF1Bskill \u52A0\u8F7D\u6052\u53EF\u7528\uFF09\u3002\ntrajectory \u7EAA\u5F8B\u5B50\u96C6\uFF08session/keeper/rebuild \u53C2\u4E0E\u9879\uFF09\u9ED8\u8BA4\u5168\u5173 = \u5B8C\u5168\u72EC\u7ACB\u3002\nsystemPrompt \u5185\u8054 \u6216 systemPromptFile\uFF08.md \u5F15\u7528\uFF0C\u63A8\u8350\uFF09\u2014\u2014\u89D2\u8272\u4F1A\u8BDD\u7684\u5B8C\u6574\u4EBA\u683C/\u8FB9\u754C\u63D0\u793A\u8BCD\u3002\nvalidate \u6821\u9A8C / apply \u751F\u6548 / list \u67E5\u770B\uFF1Acontainer_admin role <guide|validate|apply|list>\uFF08skiff_admin \u5DF2\u5E76\u5165\uFF09\u3002\n\n Config:\n { \"skiff\": { \"roles\": {\n \"qa\": {\n \"model\": \"provider/model\",\n \"msms\": [\"web-search\", \"vlm-describe\"],\n \"tools\": [\"read\", \"grep\", \"glob\"],\n \"systemPromptFile\": \".opencode/skiff/qa.md\"\n } } } }\n\n\u2500\u2500 7. autopilotTrajectory\uFF08\u81EA\u52A8\u5DE1\u822A\u8F68\u8FF9\uFF09\u2500\u2500\nCCC \u5B9A\u4E49\u7684\u4E00\u6761\u81EA\u4E3B trajectory\u2014\u2014\u65F6\u949F\u5230\u70B9\u81EA\u52A8\u5524\u8D77\uFF08\u524D\u53F0\u6CE8\u5165\uFF0C\u7528\u6237\u53EF\u89C1\u53EF\u4ECB\u5165\uFF09\u3002\n\u672A\u914D\u7F6E\u6216 enabled=false \u2192 \u5B8C\u5168\u4E0D\u542F\u52A8\uFF08\u96F6\u8D44\u6E90\u5360\u7528\uFF09\u3002\u591A CCC \u72EC\u7ACB\uFF1A\u6BCF CCC \u81EA\u5DF1\u7684\u914D\u7F6E\u3002\n\u5524\u8D77\u6D88\u606F\u56DB\u6BB5\u5F0F\uFF1A\u8F68\u8FF9\u7126\u70B9 topPrompt\uFF08\u6700\u5148\u6CE8\u5165\uFF0C\u7A33\u5B9A\u951A\uFF09\u2192 \u8EAB\u4EFD\u951A\u5B9A \u2192 \u5148\u9A8C\u504F\u89C1\n\uFF08CCC \u6839\u811A\u672C biasProvider \u8F93\u51FA\uFF09\u2192 \u4EFB\u52A1\u3002\u76EE\u6807\u4F1A\u8BDD session \u5FC5\u586B\uFF08\u76EE\u5F55\u987B\u5E26 --auto \u540E\u7F00\uFF09\u3002\n\u8BCA\u65AD/\u72B6\u6001/\u7ACB\u5373\u5524\u8D77\uFF1Aautopilot-trajectory <all|check|status|diag>\uFF08msm(\"autopilot-trajectory\", [\"all\"])\uFF09\u3002\n\n Config:\n { \"autopilotTrajectory\": {\n \"enabled\": true,\n \"intervalHours\": 2,\n \"session\": \"S151\",\n \"biasProvider\": \"autopilot-bias.ts\",\n \"topPrompt\": \"\u672C\u8F68\u8FF9\u6838\u5FC3\u76EE\u6807/\u7EAA\u5F8B/\u8D28\u91CF\u8981\u6C42\uFF08CCC \u81EA\u586B\uFF0C\u9632\u6F02\u79FB\uFF09\",\n \"avoidWakeHours\": { \"start\": 8, \"end\": 18 }\n } }\n\n\u2500\u2500 8. weixin\uFF08\u5FAE\u4FE1\u6865 F4c-3\uFF0C\u542B\u6D88\u606F\u8BB0\u5F55 hook\uFF09\u2500\u2500\nCCC \u7EA7\u5FAE\u4FE1\u4E2A\u4EBA\u53F7\u63A5\u5165\uFF08iLink \u534F\u8BAE\uFF09\uFF1Adsh \u4E00\u8FDB\u7A0B\u591A CCC\uFF0C\u6BCF CCC \u72EC\u7ACB\u5BF9\u63A5\u5FAE\u4FE1\u6865\u3002\n\u8D26\u53F7/\u8DEF\u7531/\u5F00\u5173\u5728\u6B64\u6587\u4EF6\uFF1B**bot_token \u51ED\u636E\u5728 CCC localstore credential scope**\n\uFF08\u626B\u7801\u7ED1\u5B9A\u540E\u81EA\u52A8\u5199\u5165\uFF0C\u6C38\u4E0D\u8FDB git \u660E\u6587\u9762\uFF09\u3002\n\u8DEF\u7531 user \u2192 role\uFF1Aexact \u4F18\u5148\uFF0C* \u901A\u914D\u515C\u5E95\uFF1Brole \u5FC5\u987B \u2208 \u8BE5 CCC skiff.roles\u3002\n\u9762\u677F\uFF08WebUI \u8BBE\u7F6E \u2192 \u5FAE\u4FE1\u6865\uFF09\u53EF\u626B\u7801\u7ED1\u5B9A/\u79FB\u9664\u8D26\u53F7/\u7F16\u8F91\u8DEF\u7531\uFF1Bmsm(\"weixin-doctor\", [\"status\"|\"diag\"|\"verify\"|\"guide\"]) \u6392\u67E5 + \u5927\u800C\u5168\u6307\u5357\u3002\u51ED\u636E\u4E3B\u52A8\u67E5\u770B\uFF1Alocalstore get WEIXIN_<ACCOUNT>_TOKEN\u3002\n \u25B8 \u4E3B\u52A8\u53D1\u9001\uFF08v1.30.9\uFF09\uFF1ACCC \u7528 msm(\"weixin-send\", [\"send\", \"--ccc\", \"<CCC>\", \"--user\", \"<\u522B\u540D|id>\", \"<\u6587\u672C>\"])\n \u7ED9\u7528\u6237\u53D1\u6D88\u606F\u2014\u2014\u7ECF dsp \u4E3B\u52A8\u53D1\u9001\u5165\u53E3\uFF08**\u53EA\u7ED1 127.0.0.1**\uFF0Cplugin \u5168\u5C40\u914D\u7F6E ~/.dsh/serenity-hooks.json\n \u7684 weixinApi.port\uFF0C\u9ED8\u8BA4 3082\uFF1B\u516C\u7F51\u4E0D\u53EF\u8FBE\u6545\u65E0\u5BC6\u94A5\uFF09\u2192 weixin-bridge.sendProactiveText \u2192\n \u6210\u529F\u540E\u89E6\u53D1 outgoing hook\uFF08source=\"proactive\"\uFF09\u3002send-file \u4ECD\u7531 MSM \u76F4\u8FDE iLink \u4E0A\u4F20\u5E76\u81EA\u884C\u8865\u8BB0\u3002\n\n Config:\n { \"weixin\": {\n \"enabled\": true,\n \"hook\": \"scripts/weixin-message-hook.ts\",\n \"accounts\": [{ \"accountId\": \"wechat-1\", \"name\": \"\u5BB6\u5EAD\u52A9\u624B\", \"enabled\": true }],\n \"routes\": [{ \"user\": \"*\", \"role\": \"zhaocai\" }]\n } }\n\n \u25B8 weixin.hook\uFF08\u6D88\u606F\u8BB0\u5F55 hook\uFF0Cv1.27.13\uFF09\uFF1A\n \u5FAE\u4FE1\u6865\u6BCF\u6536/\u53D1\u4E00\u6761\u6D88\u606F\u89E6\u53D1\u4E00\u6B21 CCC \u81EA\u5199\u811A\u672C\uFF0C\u7531 CCC \u81EA\u884C\u6301\u4E45\u5316\u4FDD\u5B58\uFF08\u5B58\u54EA/\u5B58\u6210\u4EC0\u4E48\n /\u662F\u5426\u5165\u5E93\u5168\u5F52 CCC\u2014\u2014ACC \u4E0D\u7ED1\u5B9A\u5B58\u50A8\uFF09\u3002\u672A\u914D\u7F6E hook \u2192 \u96F6\u53D8\u5316\uFF08\u4E0D\u89E6\u53D1\uFF09\u3002\n\n \u89E6\u53D1\uFF08\u53CC\u5411\uFF09\uFF1A\n incoming = \u7528\u6237 \u2192 bot\uFF1A\u8DEF\u7531\u547D\u4E2D\u540E\u3001\u5A92\u4F53\u843D\u76D8\u540E\u89E6\u53D1\uFF08\u6587\u672C\u542B\u8BED\u97F3\u8F6C\u5199\uFF1B\u5A92\u4F53\u5E26\u843D\u76D8 relPath\uFF09\n outgoing = bot \u2192 \u7528\u6237\uFF1A\u53D1\u9001\u6210\u529F\u540E\u89E6\u53D1\uFF08reply = \u7528\u6237\u5B9E\u9645\u6536\u5230\u7684\u7EAF\u6587\u672C\uFF0C\u5DF2\u5265\u79BB think\uFF09\n \u00B7 source=\"reply\"\uFF08\u7F3A\u7701\uFF09= \u56DE\u590D\u7528\u6237\u6D88\u606F\uFF1Bsource=\"proactive\"\uFF08v1.30.9\uFF09= bot \u4E3B\u52A8\u53D1\u8D77\n \uFF08CCC \u7ECF weixin-send MSM \u2192 dsp \u4E3B\u52A8\u53D1\u9001\u5165\u53E3 \u2192 sendProactiveText\uFF09\n \u00B7 file\uFF08\u4EC5 send-file \u8865\u8BB0\uFF09\uFF1A{ \"name\": \"\u62A5\u544A.pdf\", \"size\": 12345, \"caption\": \"\u53EF\u9009\" }\n\n \u811A\u672C\u7EA6\u5B9A\uFF08\u4E8B\u4EF6 JSON \u5355\u884C\u7ECF stdin \u4F20\u5165\uFF1Bbun \u4F18\u5148 node \u515C\u5E95\uFF09\uFF1A\n #!/usr/bin/env bun \u6216 node script.js \u2014\u2014 \u8BFB process.stdin \u6574\u6BB5 JSON.parse\n const ev = JSON.parse(await new Response(process.stdin).text())\n ev.event === 'incoming' | 'outgoing'\n\n \u4E8B\u4EF6 schema\uFF08**\u4E0D\u542B\u4EFB\u4F55\u4F1A\u8BDD\u51ED\u636E**\u2014\u2014context_token/bot_token/token/aes_key \u5747\u4E0D\u51FA\u73B0\uFF09\uFF1A\n { \"event\": \"incoming\", \"ts\": 1788359941488, \"cccRoot\": \"/path/ccc\",\n \"accountId\": \"wechat-1\", \"userId\": \"u1@im.wechat\",\n \"sessionId\": \"skiff-weixin-xxx\", \"role\": \"zhaocai\",\n \"message\": { \"text\": \"\u4F60\u597D\", \"media\": [{ \"kind\": \"image\", \"relPath\": \"_tmp/weixin-inbound/<hash>/img_x.jpg\" }] } }\n { \"event\": \"outgoing\", \"ts\": ..., \"cccRoot\": ..., \"accountId\": ...,\n \"userId\": ..., \"sessionId\": ..., \"role\": ...,\n \"reply\": \"\u5DF2\u8BB0\u5F55\uFF08\u7EAF\u6587\u672C\uFF09\", \"source\": \"reply\" | \"proactive\" }\n\n \u793A\u4F8B\u811A\u672C\uFF08\u8FFD\u52A0\u5230\u6309\u65E5\u6587\u4EF6\u2014\u2014\u6301\u4E45\u5316\u5F52 CCC \u81EA\u9009\uFF1A\u6587\u4EF6/DB/\u8FDC\u7AEF\u5747\u53EF\uFF09\uFF1A\n const fs = require('node:fs'); const p = '/path/ccc/AGENT_SESSIONS/_weixin-log.jsonl';\n fs.appendFileSync(p, JSON.stringify(ev) + '\\n');\n\n \u6267\u884C\u8BED\u4E49\uFF08\u65C1\u8DEF\u5BB9\u5FCD\uFF09\uFF1A\u5F02\u6B65 fire-and-forget + 15s \u8D85\u65F6 kill + \u5931\u8D25\u4EC5\u65E5\u5FD7\u2014\u2014\n \u5FAE\u4FE1\u6865\u6D88\u606F\u5904\u7406/\u56DE\u590D\u4E0D\u53D7 hook \u5F71\u54CD\uFF1B\u811A\u672C\u987B\u5728 CCC \u6839\u5185\uFF08\u8DEF\u5F84\u9003\u9038\u62D2\u7EDD\uFF09\u3002\n \u5A92\u4F53 relPath \u6307\u5411 _tmp/weixin-inbound/\uFF08\u4E34\u65F6\u76EE\u5F55\uFF09\u2014\u2014\u9700\u6301\u4E45\u4FDD\u5B58\u5A92\u4F53\u6587\u4EF6\u8BF7\u81EA\u884C copy\u3002\n";
|
|
23
23
|
export interface MsmFlag {
|
|
24
24
|
name: string;
|
|
25
25
|
type?: string;
|
package/lib/skiff-core.d.ts
CHANGED
|
@@ -40,13 +40,21 @@ export declare function workspaceTrajectoryLine(mdPath: string): string;
|
|
|
40
40
|
* 懒绑定 + 幂等(per skiff 会话 scope 隔离——微信多用户各自独立,永不共享同一份)。
|
|
41
41
|
*
|
|
42
42
|
* 判定链:
|
|
43
|
+
* ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
|
|
44
|
+
* (ACP/调试页/问答页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
|
|
45
|
+
* 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
|
|
46
|
+
* 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
|
|
43
47
|
* ① 角色未启用 session 能力(trajectory.session !== true)→ null(现状零变化)
|
|
44
|
-
* ②
|
|
48
|
+
* ② 已有**本机制自动创建**的 bound(note 前缀 auto-created)→ 恢复(重启/续接记得)
|
|
49
|
+
* ②b 内存活跃记录(同 scope)→ 恢复 + **自愈补写绑定**(防御纵深:绑定写失败/事件形态未落盘时
|
|
50
|
+
* 仍复用同一 SESSION,而不是每条消息新建一个——v1.30.5 及更早"每条消息一个 SESSION"的放大效应)
|
|
45
51
|
* ③ 无 → 自动创建专属 SESSION(createSession desc=`<role> skiff`——编号自动递增,用户不关心是谁)
|
|
46
52
|
*
|
|
53
|
+
* @param persistent 该 skiff 会话是否有**持久身份**(固定会话 id)。默认 true(向后兼容调用点);
|
|
54
|
+
* `createSkiffAgent` 传 `sessionId !== undefined`,微信桥的 existing 快路径传 true。
|
|
47
55
|
* @returns 绑定后的 SESSION.md 绝对路径(供提示词注入);不适用/失败 → null
|
|
48
56
|
*/
|
|
49
|
-
export declare function ensureSkiffSession(root: string, agent: Agent, roleName: string, role: SkiffRoleConfig): string | null;
|
|
57
|
+
export declare function ensureSkiffSession(root: string, agent: Agent, roleName: string, role: SkiffRoleConfig, persistent?: boolean): string | null;
|
|
50
58
|
/** 当前 scope 是否已激活(幂等判定:getActiveSessionInfo 命中同 dirName)——测试用 */
|
|
51
59
|
export declare function skiffSessionActiveFor(sessionId: string): {
|
|
52
60
|
sessionId: string;
|
|
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
|
2
2
|
import { a as findSerenityRoot, f as readHandymanConfig } from "./ccc-DAsSHsub.js";
|
|
3
3
|
import { a as resolveRoleSystemPrompt, i as readSkiffRoles, n as buildSkiffBasePrompt, o as roleMsmWhitelist, r as isSkiffSessionId, u as trajectorySubset } from "./skiff-role-DlrbHPLD.js";
|
|
4
4
|
import { i as hostSessions, r as hostService, t as hostAgents } from "./access-CdL6BAYj.js";
|
|
5
|
-
import { c as createSession, n as readLastBound, t as appendBound, x as setActiveSessionInfo } from "./session-bound-D2ANqVn-.js";
|
|
5
|
+
import { c as createSession, f as getActiveSessionInfo, n as readLastBound, t as appendBound, x as setActiveSessionInfo } from "./session-bound-D2ANqVn-.js";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { basename, join } from "node:path";
|
|
8
8
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
@@ -319,14 +319,23 @@ function workspaceTrajectoryLine(mdPath) {
|
|
|
319
319
|
* 懒绑定 + 幂等(per skiff 会话 scope 隔离——微信多用户各自独立,永不共享同一份)。
|
|
320
320
|
*
|
|
321
321
|
* 判定链:
|
|
322
|
+
* ⓪ **临时身份不建工作台**(v1.30.9,S142 用户报"SESSION 爆炸"根治):`persistent=false`
|
|
323
|
+
* (ACP/调试页/问答页等**无固定会话 id**的临时 agent)→ null。理由(R↓):这些 agent 每次
|
|
324
|
+
* 调用都是新 id、无身份延续,为其建 SESSION = 每次问答留一个永久空目录(无界熵增);
|
|
325
|
+
* 用户模型「skiff 本质是临时会话机制」——工作台属于**持久身份**,不属于单次调用。
|
|
322
326
|
* ① 角色未启用 session 能力(trajectory.session !== true)→ null(现状零变化)
|
|
323
|
-
* ②
|
|
327
|
+
* ② 已有**本机制自动创建**的 bound(note 前缀 auto-created)→ 恢复(重启/续接记得)
|
|
328
|
+
* ②b 内存活跃记录(同 scope)→ 恢复 + **自愈补写绑定**(防御纵深:绑定写失败/事件形态未落盘时
|
|
329
|
+
* 仍复用同一 SESSION,而不是每条消息新建一个——v1.30.5 及更早"每条消息一个 SESSION"的放大效应)
|
|
324
330
|
* ③ 无 → 自动创建专属 SESSION(createSession desc=`<role> skiff`——编号自动递增,用户不关心是谁)
|
|
325
331
|
*
|
|
332
|
+
* @param persistent 该 skiff 会话是否有**持久身份**(固定会话 id)。默认 true(向后兼容调用点);
|
|
333
|
+
* `createSkiffAgent` 传 `sessionId !== undefined`,微信桥的 existing 快路径传 true。
|
|
326
334
|
* @returns 绑定后的 SESSION.md 绝对路径(供提示词注入);不适用/失败 → null
|
|
327
335
|
*/
|
|
328
|
-
function ensureSkiffSession(root, agent, roleName, role) {
|
|
336
|
+
function ensureSkiffSession(root, agent, roleName, role, persistent = true) {
|
|
329
337
|
if (!trajectorySubset(role).session) return null;
|
|
338
|
+
if (!persistent) return null;
|
|
330
339
|
const session = agent.session;
|
|
331
340
|
const scope = String(agent.session.id ?? agent.id);
|
|
332
341
|
const lastBound = session ? readLastBound(session) : null;
|
|
@@ -338,6 +347,16 @@ function ensureSkiffSession(root, agent, roleName, role) {
|
|
|
338
347
|
});
|
|
339
348
|
return lastBound.mdPath;
|
|
340
349
|
}
|
|
350
|
+
const inMemory = getActiveSessionInfo(scope);
|
|
351
|
+
if (inMemory?.mdPath && existsSync(inMemory.mdPath)) {
|
|
352
|
+
appendBound(session, "reconcile", {
|
|
353
|
+
dirName: inMemory.dirName,
|
|
354
|
+
mdPath: inMemory.mdPath,
|
|
355
|
+
...inMemory.sessionId ? { sessionId: inMemory.sessionId } : {},
|
|
356
|
+
note: `${AUTO_BOUND_NOTE_PREFIX} ${roleName}`
|
|
357
|
+
});
|
|
358
|
+
return inMemory.mdPath;
|
|
359
|
+
}
|
|
341
360
|
const desc = `${roleName} skiff`;
|
|
342
361
|
const created = createSession({
|
|
343
362
|
root,
|
|
@@ -394,7 +413,7 @@ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, session
|
|
|
394
413
|
}
|
|
395
414
|
let workspaceMdPath = null;
|
|
396
415
|
try {
|
|
397
|
-
workspaceMdPath = ensureSkiffSession(root, agent, roleName, role);
|
|
416
|
+
workspaceMdPath = ensureSkiffSession(root, agent, roleName, role, sessionId !== void 0);
|
|
398
417
|
} catch (err) {
|
|
399
418
|
console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 专属 SESSION 处理失败(不影响 agent 创建): ${String(err?.message ?? err)}`);
|
|
400
419
|
}
|
|
@@ -25,6 +25,7 @@ var weixin_api_exports = /* @__PURE__ */ __exportAll({
|
|
|
25
25
|
MessageType: () => MessageType,
|
|
26
26
|
TypingStatus: () => TypingStatus,
|
|
27
27
|
aes128EcbDecrypt: () => aes128EcbDecrypt,
|
|
28
|
+
assertIlinkOk: () => assertIlinkOk,
|
|
28
29
|
buildClientVersion: () => buildClientVersion,
|
|
29
30
|
buildMediaDownloadUrl: () => buildMediaDownloadUrl,
|
|
30
31
|
downloadMedia: () => downloadMedia,
|
|
@@ -120,6 +121,26 @@ async function apiPost(params) {
|
|
|
120
121
|
throw err;
|
|
121
122
|
}
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* iLink 业务层返回码校验(v1.30.9 修复):`apiPost` 只判 HTTP 状态,而 iLink 在 **HTTP 200**
|
|
126
|
+
* 的响应体里用 `ret` / `errcode` 表达失败(如 `{ret:1,errmsg:"denied"}`)——此前被当作成功,
|
|
127
|
+
* 于是"发送成功"的判定是假的:回复路径会给一条**从未送达**的消息触发 outgoing hook 记录
|
|
128
|
+
* (与 weixin-bridge 注释"发送失败不记录"矛盾)。发送类调用一律经此校验。
|
|
129
|
+
* 非 JSON 的 200 响应保持既有宽容语义(视为成功)。
|
|
130
|
+
* @param endpoint 端点名(错误信息定位用)
|
|
131
|
+
* @param rawText 响应体原文
|
|
132
|
+
*/
|
|
133
|
+
function assertIlinkOk(endpoint, rawText) {
|
|
134
|
+
let json;
|
|
135
|
+
try {
|
|
136
|
+
json = JSON.parse(rawText);
|
|
137
|
+
} catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const detail = json.errmsg ? `: ${json.errmsg}` : "";
|
|
141
|
+
if (typeof json.ret === "number" && json.ret !== 0) throw new Error(`${endpoint} ret=${json.ret}${detail}`);
|
|
142
|
+
if (typeof json.errcode === "number" && json.errcode !== 0) throw new Error(`${endpoint} errcode=${json.errcode}${detail}`);
|
|
143
|
+
}
|
|
123
144
|
/** 取登录二维码(无需凭据;实证裸调可用) */
|
|
124
145
|
async function fetchQRCode(params) {
|
|
125
146
|
const botType = params.botType ?? "3";
|
|
@@ -181,10 +202,11 @@ const MessageItemType = {
|
|
|
181
202
|
FILE: 4,
|
|
182
203
|
VIDEO: 5
|
|
183
204
|
};
|
|
184
|
-
/**
|
|
205
|
+
/** 发文本消息(主动发起时可不带 context_token;md→plain 内置——微信不支持 Markdown)。
|
|
206
|
+
* v1.30.9:HTTP 200 但 `ret/errcode != 0` → 抛错(此前静默当成功,导致"记录已送达"的假象)。 */
|
|
185
207
|
async function sendTextMessage(params) {
|
|
186
208
|
const clientId = params.clientId ?? `dsp-weixin-${randomBytes(4).toString("hex")}`;
|
|
187
|
-
await apiPost({
|
|
209
|
+
assertIlinkOk("ilink/bot/sendmessage", await apiPost({
|
|
188
210
|
baseUrl: params.baseUrl,
|
|
189
211
|
endpoint: "ilink/bot/sendmessage",
|
|
190
212
|
body: JSON.stringify({ msg: {
|
|
@@ -201,7 +223,7 @@ async function sendTextMessage(params) {
|
|
|
201
223
|
} }),
|
|
202
224
|
token: params.token,
|
|
203
225
|
timeoutMs: params.timeoutMs ?? 15e3
|
|
204
|
-
});
|
|
226
|
+
}));
|
|
205
227
|
}
|
|
206
228
|
/** sendtyping status:1=TYPING(开始)0=CANCEL(结束)。
|
|
207
229
|
* ⚠️ 对齐**官方 openclaw-weixin 参考实现**(index.ts onReplyStart → status 1 /
|
package/lib/weixin-api.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ export declare function buildClientVersion(version: string): number;
|
|
|
24
24
|
export type FetchLike = typeof fetch;
|
|
25
25
|
/** 测试注入:替换全局 fetch 实现(生产零调用) */
|
|
26
26
|
export declare function __setWeixinFetchForTest(fn: FetchLike | null): void;
|
|
27
|
+
/**
|
|
28
|
+
* iLink 业务层返回码校验(v1.30.9 修复):`apiPost` 只判 HTTP 状态,而 iLink 在 **HTTP 200**
|
|
29
|
+
* 的响应体里用 `ret` / `errcode` 表达失败(如 `{ret:1,errmsg:"denied"}`)——此前被当作成功,
|
|
30
|
+
* 于是"发送成功"的判定是假的:回复路径会给一条**从未送达**的消息触发 outgoing hook 记录
|
|
31
|
+
* (与 weixin-bridge 注释"发送失败不记录"矛盾)。发送类调用一律经此校验。
|
|
32
|
+
* 非 JSON 的 200 响应保持既有宽容语义(视为成功)。
|
|
33
|
+
* @param endpoint 端点名(错误信息定位用)
|
|
34
|
+
* @param rawText 响应体原文
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertIlinkOk(endpoint: string, rawText: string): void;
|
|
27
37
|
/** 取登录二维码(无需凭据;实证裸调可用) */
|
|
28
38
|
export declare function fetchQRCode(params: {
|
|
29
39
|
baseUrl?: string;
|
|
@@ -139,7 +149,8 @@ export declare const MessageItemType: {
|
|
|
139
149
|
readonly FILE: 4;
|
|
140
150
|
readonly VIDEO: 5;
|
|
141
151
|
};
|
|
142
|
-
/**
|
|
152
|
+
/** 发文本消息(主动发起时可不带 context_token;md→plain 内置——微信不支持 Markdown)。
|
|
153
|
+
* v1.30.9:HTTP 200 但 `ret/errcode != 0` → 抛错(此前静默当成功,导致"记录已送达"的假象)。 */
|
|
143
154
|
export declare function sendTextMessage(params: {
|
|
144
155
|
baseUrl: string;
|
|
145
156
|
token: string;
|
package/lib/weixin-bridge.d.ts
CHANGED
|
@@ -43,8 +43,50 @@ export declare function handleIncoming(ctx: Context, root: string, accountId: st
|
|
|
43
43
|
export declare function syncCccBridge(ctx: Context, root: string): void;
|
|
44
44
|
/** 停止某 CCC 的桥(移除账号/禁用时) */
|
|
45
45
|
export declare function stopCccBridge(root: string): void;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* 停止全部桥(插件 dispose)
|
|
48
|
+
*/
|
|
47
49
|
export declare function stopAllBridges(): void;
|
|
50
|
+
/** 主动发送失败原因(稳定 code——入口层翻译成 HTTP 状态 + 可行动提示) */
|
|
51
|
+
export type ProactiveSendErrorCode = 'BRIDGE_DISABLED' | 'NO_ACCOUNT' | 'ACCOUNT_NOT_FOUND' | 'ACCOUNT_NOT_BOUND' | 'SEND_FAILED';
|
|
52
|
+
export interface ProactiveSendInput {
|
|
53
|
+
/** CCC 根(绝对路径;由入口层解析名称/路径后传入) */
|
|
54
|
+
root: string;
|
|
55
|
+
/** 目标用户(iLink from_user_id,形如 xxx@im.wechat;别名解析归调用方) */
|
|
56
|
+
toUserId: string;
|
|
57
|
+
/** 文本(md → 微信纯文本由 sendTextMessage 内置转换) */
|
|
58
|
+
text: string;
|
|
59
|
+
/** 发送账号(缺省 = 该 CCC 第一个启用且已绑定的账号) */
|
|
60
|
+
accountId?: string;
|
|
61
|
+
}
|
|
62
|
+
export type ProactiveSendResult = {
|
|
63
|
+
ok: true;
|
|
64
|
+
accountId: string;
|
|
65
|
+
userId: string;
|
|
66
|
+
sessionId: string;
|
|
67
|
+
role: string;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
code: ProactiveSendErrorCode;
|
|
71
|
+
error: string;
|
|
72
|
+
remediation?: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* 以 bot 身份主动给指定用户发文本(**不经用户消息触发**)。
|
|
76
|
+
*
|
|
77
|
+
* 为什么放在桥里而不是让 CCC 自己直连 iLink(用户拍板 A 方案):
|
|
78
|
+
* ① **记录归桥**——发送成功后触发既有 outgoing hook,`_weixin-logs/*.jsonl` 与对话回复
|
|
79
|
+
* 同源同格式(事件多一个 `source: 'proactive'` 标记);② 协议/账号/凭据解析单一真相源,
|
|
80
|
+
* CCC 侧不必重复实现 sendmessage;③ 多账号可选。
|
|
81
|
+
*
|
|
82
|
+
* 记录一致性:`sessionId` 取 `weixinSessionIdFor(toUserId)`——与该用户平时对话**同一条轨迹**,
|
|
83
|
+
* 记录里能直接按会话串联;`role` 取该 CCC 路由命中角色(未命中 → 空串)。
|
|
84
|
+
*
|
|
85
|
+
* 主动消息不带 context_token(iLink 接受 bot 主动发起;带 token 的路径见 handleIncoming 回复)。
|
|
86
|
+
* @param input CCC 根 + 目标用户 + 文本 +(可选)账号
|
|
87
|
+
* @returns 成功含 accountId/userId/sessionId/role;失败含稳定 code 与可行动提示(不抛错)
|
|
88
|
+
*/
|
|
89
|
+
export declare function sendProactiveText(input: ProactiveSendInput): Promise<ProactiveSendResult>;
|
|
48
90
|
/** 桥状态快照(面板数据源):每 CCC → 每账号 → 轮询健康 */
|
|
49
91
|
export declare function weixinBridgeStatus(): Array<{
|
|
50
92
|
ccc: string;
|
package/lib/weixin-hook.d.ts
CHANGED
|
@@ -52,6 +52,13 @@ export interface WeixinHookIncomingEvent extends WeixinHookEventBase {
|
|
|
52
52
|
export interface WeixinHookOutgoingEvent extends WeixinHookEventBase {
|
|
53
53
|
event: 'outgoing';
|
|
54
54
|
reply: string;
|
|
55
|
+
/**
|
|
56
|
+
* 触发来源(v1.30.9,S142 用户需求"微信桥支持被调用发消息"):
|
|
57
|
+
* - `reply`(缺省,向后兼容)= 收到用户消息后的回复
|
|
58
|
+
* - `proactive` = 桥被调用主动发给指定用户(CCC MSM 经本机入口发起)
|
|
59
|
+
* 记录侧可据此区分"对话回复"与"主动留言"。
|
|
60
|
+
*/
|
|
61
|
+
source?: 'reply' | 'proactive';
|
|
55
62
|
}
|
|
56
63
|
/** 全部 hook 事件(判别联合) */
|
|
57
64
|
export type WeixinHookEvent = WeixinHookIncomingEvent | WeixinHookOutgoingEvent;
|
|
@@ -75,6 +82,8 @@ export interface OutgoingHookInput {
|
|
|
75
82
|
sessionId: string;
|
|
76
83
|
role: string;
|
|
77
84
|
reply: string;
|
|
85
|
+
/** 缺省 'reply'(对话回复);'proactive' = 桥被调用主动发送 */
|
|
86
|
+
source?: 'reply' | 'proactive';
|
|
78
87
|
}
|
|
79
88
|
/** 构造 incoming 事件对象(纯函数,可测) */
|
|
80
89
|
export declare function buildIncomingHookEvent(input: IncomingHookInput): WeixinHookIncomingEvent;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weixin-send-api.ts — 主动发送入口(S142 v1.30.9,用户需求"微信桥支持被调用发消息给指定用户")
|
|
3
|
+
*
|
|
4
|
+
* 形态(用户拍板 A2):**独立监听器,只绑 127.0.0.1**(默认 3082,plugin 全局配置 weixinApi)。
|
|
5
|
+
* 为什么不挂在主 webServer(3080) 上:3080 会被 3081 公网网关原样反代(含全部请求头),
|
|
6
|
+
* 而 3081 的登录态属于"家庭账号"——一旦挂上去,任何已登录的外部用户都能冒充 bot 发消息。
|
|
7
|
+
* 独立 loopback 监听器则**不经网关、公网不可达**,因此不做共享密钥;仍校验来源地址。
|
|
8
|
+
*
|
|
9
|
+
* 调用者 = CCC 自己的 MSM(`msm weixin-send ...`):ACC **不新增工具**,只多一个本机 HTTP 面。
|
|
10
|
+
* 发送与记录都经 `weixin-bridge.sendProactiveText` → 复用既有 outgoing hook(`source: 'proactive'`)。
|
|
11
|
+
*
|
|
12
|
+
* 契约:
|
|
13
|
+
* POST /send { ccc: "<名称|绝对路径>", user: "<from_user_id>", text: "...", accountId?: "wechat-1" }
|
|
14
|
+
* → 200 { ok: true, accountId, userId, sessionId, role }
|
|
15
|
+
* → 4xx/5xx { ok: false, code, error, remediation? }
|
|
16
|
+
* GET /health → 200 { ok: true, port }
|
|
17
|
+
*
|
|
18
|
+
* 职责切分(E↑):**别名 → id 的解析归 CCC**(MSM 读该 CCC localstore 的 WEIXIN_USER_*);
|
|
19
|
+
* 本入口只接受 iLink 形式的 id。协议、账号选择、记录归桥。
|
|
20
|
+
*/
|
|
21
|
+
import { type ServerResponse } from 'node:http';
|
|
22
|
+
import type { Context } from 'cordis';
|
|
23
|
+
import { weixinSendEndpoint } from './weixin-send-endpoint.js';
|
|
24
|
+
export { weixinSendEndpoint };
|
|
25
|
+
/** 文本长度上限(微信侧单条消息实践上限;超限拒绝而非静默截断) */
|
|
26
|
+
export declare const WEIXIN_SEND_MAX_TEXT = 4000;
|
|
27
|
+
/** 当前入口地址(MSM 环境变量注入用;未启动 → null)——状态在叶模块(msm-ops 零依赖读取) */
|
|
28
|
+
/** 测试辅助:读取当前监听端口(生产零调用) */
|
|
29
|
+
export declare function weixinSendPort(): number | null;
|
|
30
|
+
/** 来源必须是 loopback(127.0.0.1 / ::1 / IPv4-mapped) */
|
|
31
|
+
export declare function isLoopbackAddress(address: string | undefined): boolean;
|
|
32
|
+
export interface ParsedSendRequest {
|
|
33
|
+
ok: true;
|
|
34
|
+
ccc: string;
|
|
35
|
+
user: string;
|
|
36
|
+
text: string;
|
|
37
|
+
accountId?: string;
|
|
38
|
+
}
|
|
39
|
+
export type ParseResult = ParsedSendRequest | {
|
|
40
|
+
ok: false;
|
|
41
|
+
error: string;
|
|
42
|
+
remediation?: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* 校验并归一 /send 请求体(纯函数,可测)。
|
|
46
|
+
* 只做**结构**校验:ccc 必填(无隐式当前 CCC——用户拍板)、user 必须是 iLink id 形式、text 非空且不超限。
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseSendRequest(body: unknown): ParseResult;
|
|
49
|
+
/** CCC 候选(名称/路径解析用) */
|
|
50
|
+
export interface CccCandidate {
|
|
51
|
+
root: string;
|
|
52
|
+
/** basename(工作目录名) */
|
|
53
|
+
dirName: string;
|
|
54
|
+
/** `.serenity` 首行声明的 CCC 名(可能为 null) */
|
|
55
|
+
cccName: string | null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 按名称/路径解析 CCC 根(纯函数,可测)。
|
|
59
|
+
* 匹配顺序:① 绝对路径精确/上溯 ② 目录名 ③ `.serenity` 首行名(大小写不敏感)。
|
|
60
|
+
* 歧义(多个同名)→ 报错并列出候选(不猜——用户拍板"ccc 必须指定")。
|
|
61
|
+
*/
|
|
62
|
+
export declare function matchCcc(input: string, candidates: CccCandidate[]): {
|
|
63
|
+
ok: true;
|
|
64
|
+
root: string;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
error: string;
|
|
68
|
+
remediation?: string;
|
|
69
|
+
};
|
|
70
|
+
/** 单请求处理(导出供测试直接驱动,不经真实端口) */
|
|
71
|
+
export declare function handleWeixinSendRequest(ctx: Context, req: {
|
|
72
|
+
method?: string;
|
|
73
|
+
url?: string;
|
|
74
|
+
socket?: {
|
|
75
|
+
remoteAddress?: string;
|
|
76
|
+
};
|
|
77
|
+
[Symbol.asyncIterator]?: unknown;
|
|
78
|
+
}, res: ServerResponse): Promise<void>;
|
|
79
|
+
/** 启动监听器(仅 127.0.0.1);已启动 → 幂等返回 */
|
|
80
|
+
export declare function startWeixinSendApi(ctx: Context, port: number): Promise<number>;
|
|
81
|
+
/** 停止监听器(幂等) */
|
|
82
|
+
export declare function stopWeixinSendApi(): void;
|
|
83
|
+
/**
|
|
84
|
+
* 装配(index.ts apply 调用):按 plugin 全局配置 weixinApi 启停,配置变化热同步。
|
|
85
|
+
* 默认启用(仅 loopback,零公网暴露);`enabled:false` 或 `port:0` 关闭。
|
|
86
|
+
*/
|
|
87
|
+
export declare function registerWeixinSendApi(ctx: Context): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weixin-send-endpoint.ts — 主动发送入口地址的**零依赖叶模块**(v1.30.9,S142)
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个模块(R↓):地址的读者是 `msm-ops.buildMsmEnv`(把地址注入 MSM 子进程 env),
|
|
5
|
+
* 写者才是 `weixin-send-api`(监听器启停)。若 msm-ops 直接 import weixin-send-api,
|
|
6
|
+
* 静态依赖链会拖进整条微信桥栈(weixin-bridge → skiff-core → `@deepseek-ai/dsh-llm`)——
|
|
7
|
+
* 后果实测:`acc-extras` / `ops` / `skiff-admin` 三个测试套件因解析不到宿主包而**整文件加载失败**
|
|
8
|
+
* (它们只用到 MSM 注册表逻辑)。叶模块把「读」与「写」解耦:读者零依赖,写者自己承担重量。
|
|
9
|
+
*
|
|
10
|
+
* 同款先例:`host/effect.ts`(拆卸登记)、`agent-idle.ts`(等待结算)。
|
|
11
|
+
*/
|
|
12
|
+
/** 设置当前入口地址(weixin-send-api 启停时调用;null = 未启动) */
|
|
13
|
+
export declare function setWeixinSendEndpoint(value: string | null): void;
|
|
14
|
+
/** 当前入口地址(形如 http://127.0.0.1:3082;未启动 → null) */
|
|
15
|
+
export declare function weixinSendEndpoint(): string | null;
|
|
16
|
+
/** 测试辅助:重置(进程内单例,测试间隔离用) */
|
|
17
|
+
export declare function resetWeixinSendEndpointForTest(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.30.
|
|
3
|
+
"version": "1.30.9",
|
|
4
4
|
"description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|