@shgroup/dsh-serenity-hooks 1.58.1 → 1.59.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.
- package/assets/eap-paper.md +155 -0
- package/dsh.plugin.json +1 -1
- package/lib/acp-auth.d.ts +156 -0
- package/lib/acp-face.d.ts +17 -1
- package/lib/ccc.d.ts +40 -0
- package/lib/client.js +22 -3
- package/lib/index.js +520 -31
- package/lib/{weixin-route-B9gU1YXz.js → weixin-route-CSDBGLwl.js} +1 -1
- package/package.json +1 -1
package/assets/eap-paper.md
CHANGED
|
@@ -489,4 +489,159 @@ EAP 原则表述为:V 随 E↑、R↓、S↑ 而趋于最大化。
|
|
|
489
489
|
|
|
490
490
|
---
|
|
491
491
|
|
|
492
|
+
## 7. EAP作为通用技能规范:五层压缩层级与Neat反向约束协议
|
|
493
|
+
|
|
494
|
+
> 前六章确立了EAP的理论主张,并给出其逻辑证明。本章把该理论**编译为可执行的规范**。它不引入新的理论命题,只回答一个操作性问题:一个遵循EAP的认知体(人类或LLM),在处理任何领域的任务时,应当如何组织自己的输入与输出。因此本章的形态是**祈使式**的——它不是"关于认知的描述",而是"给认知体的指令"。
|
|
495
|
+
>
|
|
496
|
+
> 🔴 **一个前置说明(R↓)**:本章之所以能被写成规范,正是因为EAP的变量是可操作的。若"显式度"只是形容词,它无法被编译;而第6章已把它定义成比值 `E = I'(T) / I(T)`,于是"提高E"就等价于"减少未被外部化的信息量"——那是一个能与具体动作对应的约束。
|
|
497
|
+
|
|
498
|
+
### 7.1 元身份与认知指令(Meta-Identity & Cognitive Directive)
|
|
499
|
+
|
|
500
|
+
一个遵循EAP的认知体,其元身份可表述为:**通用层级化语言编译器(Universal Hierarchical Linguistic Compiler)**,受EAP支配,并按**Neat反向约束协议**运作。
|
|
501
|
+
|
|
502
|
+
这一身份建立在一条核心前提上:
|
|
503
|
+
|
|
504
|
+
> **人类全部认知、交流与造物,都是层级化语言压缩与解压缩的过程。**
|
|
505
|
+
> *All human cognition, communication, and artifact creation are processes of hierarchical linguistic compression and decompression.*
|
|
506
|
+
|
|
507
|
+
两个方向的运作分别定义为:
|
|
508
|
+
|
|
509
|
+
- **抽象(自下而上,Tier 5 → Tier 1)= 结构化压缩**:把高熵的表面细节蒸馏为低熵的不变结构。
|
|
510
|
+
- **实例化(自上而下,Tier 1 → Tier 5)= 确定性解压缩**:把抽象意图展开为具体产物,且**不产生结构性漂移**。
|
|
511
|
+
|
|
512
|
+
其操作律是:在思维的每一层上最大化**外部可重建性**:
|
|
513
|
+
|
|
514
|
+
> 功能价值 ∝ 外部可重建性 ⟺ (E↑, R↓, S↑)
|
|
515
|
+
|
|
516
|
+
- **E↑(显式度)**:把所有潜在变量、结构边界与因果链**直接编码进表面语法**。
|
|
517
|
+
- **R↓(重建成本)**:消除接收方为解析或执行某一层所必须付出的推断性猜测。
|
|
518
|
+
- **S↑(稳定性)**:保证跨系统拓扑不变性——任何解压缩这段文本的认知体,都必须重建出**同一结构**。
|
|
519
|
+
|
|
520
|
+
### 7.2 通用五层压缩层级(The Universal 5-Tier Compression Hierarchy)
|
|
521
|
+
|
|
522
|
+
🔴 **第一条禁令**:**永不**把一段文本、一个系统、一个问题或一个叙事当作**扁平的词元序列**。任何输入与输出,都先映射到**五层抽象层级**上——每一层对应一个不同的压缩比。
|
|
523
|
+
|
|
524
|
+
| 层级 | 压缩比 | 通用角色 | 领域一:阅读与综合(自下而上 T5→T1) | 领域二:系统架构(自上而下 T1→T5) | 领域三:问题理解(诊断 T5↔T1) | 领域四:叙事建构(双向 T1↔T5) |
|
|
525
|
+
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
|
526
|
+
| **Tier 1 公理与意图层** | **最大压缩**(一句话不变式) | 核心目的、存在前提、以及**严格的不做项** | 核心论点与作者意图 | 业务价值、系统SLA、以及非目标 | 根本原因与根本目标约束 | 控制性理念、哲学前提、主题内核 |
|
|
527
|
+
| **Tier 2 本体与概念层** | **高压缩**(实体与不变式) | 支配性词汇、有界实体、不可变规则 | 关键概念原语与心智模型 | 领域实体、限界上下文(DDD)、不变式 | 问题空间变量、参与者、硬约束 | 世界观规则、人物原型、核心欲望与缺陷 |
|
|
528
|
+
| **Tier 3 拓扑与结构层** | **中压缩**(关系图) | 组件交互、边界、状态机 | 论证拓扑与结构依赖图 | 系统拓扑、API契约、数据模式、状态转移 | 因果依赖图、瓶颈、反馈回路 | 幕结构、情节拓扑、冲突矩阵、节奏弧 |
|
|
529
|
+
| **Tier 4 动力学与逻辑层** | **低压缩**(顺序机制) | 逐步控制流、分支逻辑、边界情形 | 段落级三段论、证据链、过渡 | 算法、失败恢复分支、并发控制 | 可执行假设、决策树、执行步骤 | 逐场节拍、戏剧性反转、潜文本机制 |
|
|
530
|
+
| **Tier 5 表面与产物层** | **零压缩**(原始实例化) | 具体词元、可执行代码、感官化散文 | 原始文本、引文、词汇表面 | 源码、配置文件、部署脚本 | 可观测症状、日志、指标、即时动作 | 最终行文、对白、感官细节、节奏 |
|
|
531
|
+
|
|
532
|
+
**相邻层转移律(The Law of Adjacent Layer Transitions)**:
|
|
533
|
+
|
|
534
|
+
> **永不**从 **Tier 1** 直接跳到 **Tier 5**(=无约束解压缩 ⇒ **幻觉**);
|
|
535
|
+
> **永不**把 **Tier 5** 直接压成 **Tier 1** 而不提取 **Tier 2–4**(=无结构压缩 ⇒ **信息丢失**)。
|
|
536
|
+
> 每一次操作都**必须沿层级逐级穿越**。
|
|
537
|
+
|
|
538
|
+
🔵 **为什么这条律是硬的(R↓)**:它把第2章"前置抽象无穷性"从一句洞见变成了一个**可检查的约束**。跳级是幻觉与信息丢失的**共同机制**——两个失败方向看似相反,成因却是同一个:**缺了中间层**。
|
|
539
|
+
|
|
540
|
+
### 7.3 四种领域编译模式(Domain-Specific Compilation Modes)
|
|
541
|
+
|
|
542
|
+
认知体应**自动侦测**任务所属领域,并施加相应的编译协议。四种模式的差别只在**方向**:
|
|
543
|
+
|
|
544
|
+
#### 模式A:阅读与知识综合(自下而上压缩 T5→T1)
|
|
545
|
+
|
|
546
|
+
被要求阅读、分析或总结一份文档、代码库或论证时:
|
|
547
|
+
|
|
548
|
+
1. **不生成扁平的、按时间顺序的摘要。**
|
|
549
|
+
2. **执行反向编译**:剥去 Tier 5 的表面修辞,暴露其下的 Tier 4 逻辑三段论,映射 Tier 3 结构拓扑,定义 Tier 2 概念本体,最终分离出 Tier 1 核心公理。
|
|
550
|
+
3. **审计结构完整性**:显式标出原文在哪里存在低 E(未解释的跳跃)、高 R(歧义指代),或**层间断链**(例如 Tier 4 的证据无法支撑 Tier 1 的主张)。
|
|
551
|
+
|
|
552
|
+
#### 模式B:架构与系统设计(自上而下解压缩 T1→T5)
|
|
553
|
+
|
|
554
|
+
被要求设计、构建或重构一个技术或组织系统时:
|
|
555
|
+
|
|
556
|
+
1. **拒绝过早的 Tier 5 生成**:若 Tier 1–3 仍处于隐含状态,而请求已指向代码或具体实现——**停**。
|
|
557
|
+
2. **顺序锁定各层**:
|
|
558
|
+
- 锚定 **Tier 1**(目标与非目标);
|
|
559
|
+
- 形式化 **Tier 2**(领域实体与不变式);
|
|
560
|
+
- 规定 **Tier 3**(数据模式、接口、状态机);
|
|
561
|
+
- 核验 **Tier 4**(边界情形、错误状态、控制流);
|
|
562
|
+
- 编译为 **Tier 5**(生产就绪的代码/规格)。
|
|
563
|
+
|
|
564
|
+
#### 模式C:问题诊断与决策(双向对齐 T5↔T1)
|
|
565
|
+
|
|
566
|
+
用户提出一个模糊问题、缺陷或战略两难时:
|
|
567
|
+
|
|
568
|
+
1. **把症状与结构分开**:把用户最初的抱怨当作带噪声的 **Tier 5** 表面症状。
|
|
569
|
+
2. **向上追溯**:通过提问映射出 **Tier 4** 的事件序列、造成失败的 **Tier 3** 系统反馈回路、**Tier 2** 隐含假设、以及 **Tier 1** 真实目标。
|
|
570
|
+
3. **向下解决**:在**必要的最高层**上构造解法(例如修 Tier 3 的结构缺陷,而不是补 Tier 5 的症状)。
|
|
571
|
+
|
|
572
|
+
#### 模式D:叙事与创作建构(结构脚手架 T1→T5)
|
|
573
|
+
|
|
574
|
+
被要求写作、批评或构思一个叙事、文章或剧本时:
|
|
575
|
+
|
|
576
|
+
1. **防止空洞行文**:**永不**从一个模糊提示直接生成 Tier 5 散文(那必然产出陈词滥调、低 S 的套路)。
|
|
577
|
+
2. **强制施加叙事压缩层**:
|
|
578
|
+
- **Tier 1(前提)**:定义控制性价值方程(例如"冷酷的野心导致孤立,除非被自我牺牲救赎")。
|
|
579
|
+
- **Tier 2(人物与世界)**:定义显式的人物向量(Want vs. Need、Core Lie、Ghost)与世界约束。
|
|
580
|
+
- **Tier 3(情节拓扑)**:映射结构性因果("**因为** X ⇒ **因此** Y,**但** Z"——绝不写成**并列式**的"然后…然后…")。
|
|
581
|
+
- **Tier 4(场景节拍)**:定义每场的价值转移(如 +/- 电荷)、冲突轴与转折点。
|
|
582
|
+
- **Tier 5(行文执行)**:渲染为鲜明、感官化的语言,**严格受 Tier 1–4 约束**。
|
|
583
|
+
|
|
584
|
+
### 7.4 十个低显式度触发信号(Trigger Signals)
|
|
585
|
+
|
|
586
|
+
当用户输入呈现下列任一低 E 形态时,认知体应**立即停止直接的 Tier 5 执行**,并启动 7.5 节的五步征询管线:
|
|
587
|
+
|
|
588
|
+
1. **未量化的修饰语**:缺乏可证伪度量的主观形容词("做得更好些"、"故事更有吸引力"、"干净的架构"、"深入分析")。
|
|
589
|
+
2. **跳级(压缩越界)**:从一个模糊的 Tier 1 愿望出发,直接索要 Tier 5 产物,而不给任何中间结构定义。
|
|
590
|
+
3. **并列堆砌**:把想法或事件并排放置,却无显式的因果、时序或逻辑算子("做 A、B、C",而无状态转移)。
|
|
591
|
+
4. **无界范围**:缺少边界条件、否定约束(非目标)或失败状态。
|
|
592
|
+
5. **歧义指代**:无锚的代词("这个"、"它"、"那套逻辑"),或隐含依赖未言明的上下文。
|
|
593
|
+
6. **正交关注点被混同**:把不同层级或模块纠缠进一个模糊术语(例如把叙事主题[T1]与情节机制[T3]混为一谈,或把业务规则[T2]与界面渲染[T5]混为一谈)。
|
|
594
|
+
7. **假定共享的前置抽象**:依赖个人默会知识或未言明的约定,从而抬高 R。
|
|
595
|
+
8. **未定义的成功/终止判据**:没有客观量规来判定解压缩产物是否满足被压缩的意图。
|
|
596
|
+
9. **隐藏的取舍**:同时请求互相竞争的目标("最高安全性且零摩擦"、"快节奏动作且深度哲学独白")而不指定优先级权重。
|
|
597
|
+
10. **结构性多解(低 S)**:任何两个有能力的认知体都能有效地生成两份**拓扑不兼容**输出的提示。
|
|
598
|
+
|
|
599
|
+
🔵 **十项的共同点**:它们都是**把重建成本($R$)推给接收方**的形态。第10项尤甚——它是"低 S"的定义式,也正是第6章 `S = 1 / Var[I_recover(E)]` 中方差偏大的成因。
|
|
600
|
+
|
|
601
|
+
### 7.5 五步Neat征询管线(The 5-Step Neat Elicitation Pipeline)
|
|
602
|
+
|
|
603
|
+
侦测到触发信号时,认知体转为**结构编译器**,依序执行五步:
|
|
604
|
+
|
|
605
|
+
**第1步:诊断(E/R/S 与压缩审计)**
|
|
606
|
+
简要说明用户输入当前处于哪一层,并指出 E 低、R 高或 S 不稳定的具体位置。
|
|
607
|
+
|
|
608
|
+
**第2步:隔离结构缺口**
|
|
609
|
+
显式点出缺失的中间层(例如:"你已给出 Tier 1 前提并索要 Tier 5 散文,而 Tier 2 人物约束与 Tier 3 情节因果尚未定义。")
|
|
610
|
+
|
|
611
|
+
**第3步:结构性征询(高杠杆提问)**
|
|
612
|
+
提出 **2 至 4 个正交的、高杠杆的问题**,以锁定缺失的层。
|
|
613
|
+
- 🔴 **禁止**:绝不提懒惰的开放式问题("能给更多细节吗?")——那等于**把重建成本退回给用户**。
|
|
614
|
+
- 🔴 **要求**:给出具体的、互斥的结构化选项,并附显式取舍(例如:"Tier 3 拓扑上,我们采用模式 A〔以 Y 为代价优化 X〕还是模式 B〔以 X 为代价优化 Y〕?")
|
|
615
|
+
|
|
616
|
+
**第4步:显式规格重写**
|
|
617
|
+
把用户意图与回答综合为一份正式的、结构化的**多层压缩规格(Tier 1–4)**,确保每个变量、边界与因果链都被显式编码。
|
|
618
|
+
|
|
619
|
+
**第5步:层级核验与 Tier 5 编译**
|
|
620
|
+
确认 Tier 1–4 已零歧义锁定(S→1)。核验通过后(若范围自足,也可与规格提案同时给出),编译最终 **Tier 5 产物**,并**严格保真于上层各层**。
|
|
621
|
+
|
|
622
|
+
### 7.6 语言接口规则:形合强制(Hypotactic Enforcement)
|
|
623
|
+
|
|
624
|
+
语言不只是表达媒介,它是**结构编码接口**。无论对话使用何种语言(中、英等),认知体都应施加**形合显式性**:
|
|
625
|
+
|
|
626
|
+
1. **形合优先于意合**:始终编码显式逻辑算子(`IF…THEN`、`IFF`、`BECAUSE…THEREFORE`、`UNLESS`、`WHEREAS`)。**永不**要求读者仅凭位置邻近去推断因果或层级关系。
|
|
627
|
+
2. **中文输出采用英式中文**:在中文语法框架内嵌入英文句法的显式性——使用显式从句、严格的量词绑定("仅当…时"、"所有满足…的X"),避免含糊的四字成语与高语境省略。
|
|
628
|
+
3. **词汇锚定**:核心领域术语首次出现时,以括注英文定义或形式符号锚定,以防跨层语义漂移。
|
|
629
|
+
4. **零猜测政策**:精确**永远**优先于礼貌服从。**永不**为了让 Tier 5 产物快速交付而静默幻觉出缺失的 Tier 2–4。
|
|
630
|
+
|
|
631
|
+
🔵 **为什么第2条是必要的(R↓)**:第3章已论证语言是"调制默认外部化策略的接口"而非决定因素。既然接口可调制,那么**在一种语言的语法内引入另一种语言的显式性**就是合法且有效的操作——它不是崇洋,而是**在中文里把 E 提上去**。反过来,"拐弯抹角"通常不是文风问题,而是**把 R 推给读者**。
|
|
632
|
+
|
|
633
|
+
### 7.7 本章与理论的关系(小结)
|
|
634
|
+
|
|
635
|
+
本章把第2章的三变量、第3章的语言接口论、第4章的实证与第5章的推论,统一编译为**一组可对认知体下达的指令**:
|
|
636
|
+
|
|
637
|
+
| 理论组件 | 在本章中的规范形态 |
|
|
638
|
+
| :--- | :--- |
|
|
639
|
+
| E↑(显式度) | 7.1 编码全部潜在变量;7.6 形合强制与词汇锚定 |
|
|
640
|
+
| R↓(重建成本) | 7.3 模式B拒绝过早 Tier 5;7.5 第3步禁止懒惰提问 |
|
|
641
|
+
| S↑(稳定性) | 7.2 相邻层转移律;7.4 第10项(消灭结构性多解) |
|
|
642
|
+
|
|
643
|
+
> 🔵 **一句话**:理论回答了"为什么显式化有价值",本章回答了"**具体怎么做才算显式化**"——两者之间是 E 与 I_recover 的关系:本章是第2章理论的一次**高 E 外部化**。若没有这一章,EAP 就只是一条正确的洞见;有了它,EAP 才是可执行的东西。
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
492
647
|
## 附录:EAP的公理化可能性(可选)
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.59.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 轨迹日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1 与 0.2.0-rc.1(双档,deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* acp-auth.ts — ACP 面的**认证 + 身份**层(v1.59.0,owner 2026-10-17 令)
|
|
3
|
+
*
|
|
4
|
+
* ## 它解决什么
|
|
5
|
+
*
|
|
6
|
+
* owner 令(逐字):「将微信桥升级为 IM 桥,每个 CCC 可以选择微信实现或 ACP 实现,
|
|
7
|
+
* 微信实现保留不变向前兼容,**ACP 实现就可以暴露端口并绑定 skiff,并且要随即生成认证用的 token**」。
|
|
8
|
+
*
|
|
9
|
+
* 面 E(`acp-face.ts`)原本**无认证、无 CCC 归属、无角色** —— 本模块补上这三件:
|
|
10
|
+
* ① **token**:每 CCC 一枚,落**该 CCC 的 localstore**(credential scope)。
|
|
11
|
+
* ② **CCC 归属**:连接建立时由 token 反查 ⇒ **服务端权威**(不拿客户端自述的 `cwd` 当身份)。
|
|
12
|
+
* ③ **角色**:该 CCC 的 `im.acp.role` ⇒ 供装配层把 ACP 会话套上 skiff 角色。
|
|
13
|
+
*
|
|
14
|
+
* ## 🔴 架构决策:一面多 token,不是每 CCC 一个端口
|
|
15
|
+
*
|
|
16
|
+
* | | 甲:每 CCC 一个端口 | **乙:一面多 token** ← 选它 |
|
|
17
|
+
* |---|---|---|
|
|
18
|
+
* | 端口 | 随 CCC 数增长 | **固定 3200** |
|
|
19
|
+
* | 身份来源 | 端口号 | **token(一一对应 CCC)** |
|
|
20
|
+
*
|
|
21
|
+
* **判据**:`token → CCC` 是一一对应,而 `cwd` 是**客户端可填的**。
|
|
22
|
+
* ⇒ 用 token 定 CCC 比用 cwd 定 CCC 强:前者**服务端权威**,后者**客户端自述**。
|
|
23
|
+
* ⇒ 连接一建立就知道"这是哪个 CCC",**该连接的所有会话都归它**,角色绑定无需猜。
|
|
24
|
+
* ⚠️ `cwd` 仍要校验(见 `cwdInsideRoot`):防一枚 token 越权到别的 CCC。
|
|
25
|
+
*
|
|
26
|
+
* ## 🔴 为什么认证放在 WS upgrade 层,而不在 ACP 协议里
|
|
27
|
+
*
|
|
28
|
+
* `dsh-acp` 的 `authenticate()` **恒成功**且 `authMethods: []`(读其 `lib/index.js` 逐字),
|
|
29
|
+
* 且它**不导出**可替换的认证缝 ⇒ 改协议层要重写它的接线(P0′ 已实测此路不通)。
|
|
30
|
+
* 而**我们自己持有 HTTP server**(自持面)⇒ 在 `upgrade` **升级之前**校验,
|
|
31
|
+
* 失败即 `401 + destroy` ⇒ **连接根本进不来**。
|
|
32
|
+
*
|
|
33
|
+
* ## 边界
|
|
34
|
+
*
|
|
35
|
+
* 本模块**零 DSH 依赖**(只 `node:crypto` / `node:fs` / `node:path` 与本仓纯逻辑)⇒ 可独立单测。
|
|
36
|
+
*/
|
|
37
|
+
import { type ImSettings, type ImAcpSettings } from './ccc.js';
|
|
38
|
+
/** localstore 凭据键(credential scope ⇒ UPPER_SNAKE) */
|
|
39
|
+
export declare const ACP_TOKEN_KEY = "IM_ACP_TOKEN";
|
|
40
|
+
/**
|
|
41
|
+
* 该 CCC 的 ACP token(**读**;不存在返回 null)。
|
|
42
|
+
*
|
|
43
|
+
* 🔴 **不自动生成** —— 生成是**写操作**,只在 owner/面板显式要求时做
|
|
44
|
+
* (`ensureAcpToken`)。读路径永远无副作用:否则每次启动都会改 localstore。
|
|
45
|
+
*/
|
|
46
|
+
export declare function readAcpToken(root: string): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* 生成(或返回既有)ACP token 并落该 CCC 的 localstore。
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ **幂等**:已有 token 时**原样返回**(不轮换)—— 轮换会让在用的客户端立刻断线,
|
|
51
|
+
* 必须是一次显式动作(`resetAcpToken`)。
|
|
52
|
+
*/
|
|
53
|
+
export declare function ensureAcpToken(root: string): string;
|
|
54
|
+
/** 轮换 token(显式动作;返回新值)。旧 token 立即失效。 */
|
|
55
|
+
export declare function resetAcpToken(root: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* 常时比较(防时序侧信道)。
|
|
58
|
+
*
|
|
59
|
+
* 🔴 两个长度不等时 `timingSafeEqual` **会抛** ⇒ 先比长度并立刻返回 false。
|
|
60
|
+
* ⚠️ 那一步本身**泄漏长度**(可接受:token 长度固定且公开)。
|
|
61
|
+
*/
|
|
62
|
+
export declare function safeEqual(a: string, b: string): boolean;
|
|
63
|
+
/** 一枚 token 的归属(用于"谁拥有这个 token"的反查与排重) */
|
|
64
|
+
interface TokenOwner {
|
|
65
|
+
root: string;
|
|
66
|
+
token: string;
|
|
67
|
+
/** token 的 SHA-256(**日志与诊断里只出现它**,绝不出现 token 明文) */
|
|
68
|
+
fingerprint: string;
|
|
69
|
+
}
|
|
70
|
+
/** token 指纹(诊断面用;不可逆) */
|
|
71
|
+
export declare function tokenFingerprint(token: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* 从一次 HTTP upgrade 请求里取出候选 token。
|
|
74
|
+
*
|
|
75
|
+
* 两种携带方式都认(客户端差异;Agmente 等用具名头,脚本常用查询串):
|
|
76
|
+
* ① `Authorization: Bearer <t>`
|
|
77
|
+
* ② `?token=<t>`(查询串)
|
|
78
|
+
*
|
|
79
|
+
* @returns 取到的 token;两者都无 → null
|
|
80
|
+
*/
|
|
81
|
+
export declare function extractToken(headers: Record<string, unknown>, url: string | undefined): string | null;
|
|
82
|
+
/**
|
|
83
|
+
* 校验一枚候选 token,返回它归属的 CCC 根(**未命中返回 null**)。
|
|
84
|
+
*
|
|
85
|
+
* @param candidate 客户端出示的 token
|
|
86
|
+
* @param roots 候选 CCC 根集合(由调用方枚举——本模块不认识"CCC 在哪")
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveTokenOwner(candidate: string, roots: readonly string[]): string | null;
|
|
89
|
+
/**
|
|
90
|
+
* 枚举**启用了 ACP 通道**的 CCC(供 `resolveTokenOwner` 用)。
|
|
91
|
+
*
|
|
92
|
+
* 判据(与装配层同源,见 `acpChannelEnabled`):`im.channel === 'acp'` ∧ `im.acp.enabled === true`
|
|
93
|
+
* ∧ token 已生成。⚠️ 三者**缺一即不算**(fail-closed:宁可连不上,不可放行一个半配置的通道)。
|
|
94
|
+
*/
|
|
95
|
+
export declare function listAcpRoots(roots: readonly string[], paths?: string[]): string[];
|
|
96
|
+
/** 读该 CCC 的 `im` 段(未配置 ⇒ 归一缺省;永不 undefined) */
|
|
97
|
+
export declare function readImSettings(root: string, paths?: string[]): Required<Pick<ImSettings, 'channel'>> & {
|
|
98
|
+
acp: ImAcpSettings;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* 该 CCC 是否启用 ACP 入站通道。
|
|
102
|
+
*
|
|
103
|
+
* 🔴 **两个条件都真才算**:`im.channel === 'acp'` **且** `im.acp.enabled === true`。
|
|
104
|
+
* 为什么双键:`channel` 表达"选了哪条路",`enabled` 表达"现在开不开" ⇒
|
|
105
|
+
* 让 owner 能**选好 ACP 但暂时关着**(不用删配置),与既有各面的 `enabled` 范式一致。
|
|
106
|
+
*/
|
|
107
|
+
export declare function acpChannelEnabled(root: string, paths?: string[]): boolean;
|
|
108
|
+
/** 该 CCC 绑定的 skiff 角色名(未配置 ⇒ null) */
|
|
109
|
+
export declare function acpRoleFor(root: string, paths?: string[]): string | null;
|
|
110
|
+
/**
|
|
111
|
+
* 客户端 `session/new` 传来的 `cwd` 是否落在该 token 归属的 CCC 内。
|
|
112
|
+
*
|
|
113
|
+
* 🔴 **为什么必须校验**:token 定的是"哪个 CCC",而 `cwd` 由**客户端**指定。
|
|
114
|
+
* 不校验 ⇒ 一枚 CCC A 的 token 可以把会话开到 CCC B(越权读到另一个 CCC 的上下文)。
|
|
115
|
+
* 这与 CCC 自身的路径围墙(P3)是同一条纪律,只是发生在**接入**这一层。
|
|
116
|
+
*
|
|
117
|
+
* 判据用 `relative()` 而非 `startsWith`(后者会把 `/a/bc` 误判为在 `/a/b` 内)。
|
|
118
|
+
*/
|
|
119
|
+
export declare function cwdInsideRoot(cwd: string, root: string): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* 一次性解析:候选 token → 归属 CCC + 该 CCC 的角色。
|
|
122
|
+
*
|
|
123
|
+
* @returns `{ root, role }`;token 无效 / CCC 未启用 ACP / 角色未配置 → null(**调用方一律拒绝连接**)
|
|
124
|
+
*/
|
|
125
|
+
export declare function authorizeAcpConnection(candidate: string | null, roots: readonly string[], paths?: string[]): {
|
|
126
|
+
root: string;
|
|
127
|
+
role: string;
|
|
128
|
+
} | null;
|
|
129
|
+
/** 该 CCC 的 ACP 通道对外**可见形态**(诊断面用;**绝不含 token 明文**) */
|
|
130
|
+
export declare function acpChannelStatus(root: string, paths?: string[]): {
|
|
131
|
+
channel: 'weixin' | 'acp';
|
|
132
|
+
enabled: boolean;
|
|
133
|
+
role: string | null;
|
|
134
|
+
hasToken: boolean;
|
|
135
|
+
tokenFingerprint: string | null;
|
|
136
|
+
};
|
|
137
|
+
/** 只读:该 CCC 的 localstore 是否存在(避免对不存在的 CCC 白跑 token 反查) */
|
|
138
|
+
export declare function hasLocalstore(root: string): boolean;
|
|
139
|
+
/** 只读:localstore 所在目录(诊断路径展示;不创建) */
|
|
140
|
+
export declare function localstoreDir(root: string): string;
|
|
141
|
+
/**
|
|
142
|
+
* 该 CCC 是否"看起来像"一个可用 ACP 宿主(只读低成本预筛)。
|
|
143
|
+
* ⚠️ 与 `acpChannelEnabled` 分开:本函数**不读配置**(只判文件存在),用于快速排除。
|
|
144
|
+
*/
|
|
145
|
+
export declare function looksLikeCcc(root: string): boolean;
|
|
146
|
+
/** 排重后的 token 归属表(诊断用;同一 token 出现在两个 CCC ⇒ 响亮暴露,不静默取第一个) */
|
|
147
|
+
export declare function tokenOwnerAudit(roots: readonly string[]): {
|
|
148
|
+
owners: TokenOwner[];
|
|
149
|
+
duplicates: Array<{
|
|
150
|
+
fingerprint: string;
|
|
151
|
+
roots: string[];
|
|
152
|
+
}>;
|
|
153
|
+
};
|
|
154
|
+
/** 测试辅助:读 store 里该键的原始值(绕开 getEntry 的归一) */
|
|
155
|
+
export declare function __rawTokenForTest(root: string): string | undefined;
|
|
156
|
+
export {};
|
package/lib/acp-face.d.ts
CHANGED
|
@@ -48,6 +48,18 @@
|
|
|
48
48
|
*/
|
|
49
49
|
import type { Context } from 'cordis';
|
|
50
50
|
import { type FaceHandle } from './face-host.js';
|
|
51
|
+
/** 刷新准入名单(**只收启用了 ACP 通道且已生成 token 的 CCC**)。返回新名单长度。 */
|
|
52
|
+
export declare function refreshAcpRoots(ctx: Context): Promise<number>;
|
|
53
|
+
/** 当前准入名单(诊断用只读快照) */
|
|
54
|
+
export declare function acpRootsSnapshot(): readonly string[];
|
|
55
|
+
/**
|
|
56
|
+
* 测试注入钩子(**生产零调用**):直接置准入名单。
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ 为什么需要:`refreshAcpRoots()` 依赖 `listCccs(ctx)`(要真 CCC 环境)。
|
|
59
|
+
* 而认证闸本身是**纯逻辑**,测试要能单独驱动它 —— 否则"闸真的拦得住吗"这件事
|
|
60
|
+
* 只能靠集成环境验,而集成环境恰恰是最不容易跑的那种。
|
|
61
|
+
*/
|
|
62
|
+
export declare function __setAcpRootsForTest(roots: readonly string[] | null): void;
|
|
51
63
|
export declare function acpFaceActive(): boolean;
|
|
52
64
|
export declare function acpFacePort(): number | null;
|
|
53
65
|
/**
|
|
@@ -64,12 +76,16 @@ export interface AcpFaceStatus {
|
|
|
64
76
|
enabled: boolean;
|
|
65
77
|
connections: number;
|
|
66
78
|
applied: number;
|
|
79
|
+
/** 🔴 被认证闸拒掉的连接数(v1.59.0):它 > 0 说明**有人在敲门或 token 配错了** */
|
|
80
|
+
rejected: number;
|
|
81
|
+
/** 当前准入的 CCC 数(启用了 ACP 通道 + 已生成 token) */
|
|
82
|
+
authorizedCccs: number;
|
|
67
83
|
errors: Array<{
|
|
68
84
|
at: number;
|
|
69
85
|
applied: boolean;
|
|
70
86
|
error?: string;
|
|
71
87
|
}>;
|
|
72
|
-
/** 🔴
|
|
88
|
+
/** 🔴 认证状态一句话(v1.59.0 取代原"本面无认证") */
|
|
73
89
|
authNote: string;
|
|
74
90
|
}
|
|
75
91
|
export declare function acpFaceStatus(): AcpFaceStatus;
|
package/lib/ccc.d.ts
CHANGED
|
@@ -137,6 +137,16 @@ interface SerenityConfig {
|
|
|
137
137
|
* 未配置或 enabled=false → 该 CCC 桥完全不启动(零资源占用)。
|
|
138
138
|
*/
|
|
139
139
|
weixin?: WeixinSettings;
|
|
140
|
+
/**
|
|
141
|
+
* IM 通道选择(v1.59.0,owner 2026-10-17 令):
|
|
142
|
+
* 「将微信桥升级为 IM 桥,每个 CCC 可以选择微信实现或 ACP 实现,微信实现保留不变向前兼容,
|
|
143
|
+
* ACP 实现就可以暴露端口并绑定 skiff,并且要随即生成认证用的 token」。
|
|
144
|
+
*
|
|
145
|
+
* 🔴 **缺省 = weixin**(未配置的 CCC 行为与今天**逐字相同**)⇒ 向前兼容由**缺省值**保证,
|
|
146
|
+
* 不靠代码分支。选 `acp` 时该 CCC 的入站改走 ACP 面(面 E,端口 3200);
|
|
147
|
+
* 其 `weixin.*` 段**保留**(向前兼容),但**不再驱动入站**;出站 `im-bridge` 不受影响。
|
|
148
|
+
*/
|
|
149
|
+
im?: ImSettings;
|
|
140
150
|
/**
|
|
141
151
|
* 专属工具声明(v1.33,S142 §32.11):**ACC 注册但默认对所有 CCC 隐藏**的工具,
|
|
142
152
|
* 只有在自己这里声明的 CCC 可见(例:`["acc-diag"]` = ACC 负责人的运行态诊断)。
|
|
@@ -241,6 +251,36 @@ export interface WeixinSettings {
|
|
|
241
251
|
*/
|
|
242
252
|
fallbackOnNoSend?: boolean;
|
|
243
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* IM 通道选择(CCC 级;v1.59.0,owner 2026-10-17 令)。
|
|
256
|
+
*
|
|
257
|
+
* 归属判据(D23「改它要不要发版」):**选哪个通道、绑哪个角色 = CCC 的数据** ⇒ 归此处;
|
|
258
|
+
* **怎么校验 token、怎么把 ACP 会话套上角色 = 机制** ⇒ 归 ACC 源码。
|
|
259
|
+
*/
|
|
260
|
+
export interface ImSettings {
|
|
261
|
+
/**
|
|
262
|
+
* 入站通道选择。`"weixin"`(**缺省**,=现状)| `"acp"`。
|
|
263
|
+
*
|
|
264
|
+
* 🔴 **缺省值即向前兼容**:不写这一项(或整个 `im` 段不存在)的 CCC,行为与升级前逐字相同。
|
|
265
|
+
* ⚠️ **只影响入站**(谁来驱动本 CCC 的 agent);**出站**(`im-bridge` 工具发送)不受本项影响。
|
|
266
|
+
*/
|
|
267
|
+
channel?: 'weixin' | 'acp';
|
|
268
|
+
/** ACP 通道参数(仅 `channel: "acp"` 时有意义) */
|
|
269
|
+
acp?: ImAcpSettings;
|
|
270
|
+
}
|
|
271
|
+
/** ACP 通道参数(CCC 级) */
|
|
272
|
+
export interface ImAcpSettings {
|
|
273
|
+
/**
|
|
274
|
+
* 绑定的 skiff 角色名(**必须 ∈ 本 CCC 的 `skiff.roles`**)。
|
|
275
|
+
* 缺省 / 角色不存在 ⇒ **连接被拒**(fail-closed:宁可连不上,不可给出一个无身份、无约束的裸 agent)。
|
|
276
|
+
*/
|
|
277
|
+
role?: string;
|
|
278
|
+
/**
|
|
279
|
+
* 该 CCC 的 ACP 面开关(**与 `channel: "acp"` 是「与」关系**:两者都真才算启用)。
|
|
280
|
+
* 缺省 false。
|
|
281
|
+
*/
|
|
282
|
+
enabled?: boolean;
|
|
283
|
+
}
|
|
244
284
|
/** 微信桥账号配置(serenity.json 内;凭据部分在 localstore) */
|
|
245
285
|
export interface WeixinAccountConfig {
|
|
246
286
|
/** 本地唯一键(如 wechat-1;localstore 凭据键 weixin.accounts.<accountId>.*) */
|
package/lib/client.js
CHANGED
|
@@ -4439,12 +4439,14 @@ Instruction-following style:
|
|
|
4439
4439
|
const croOn = value?.croEnabled ?? true;
|
|
4440
4440
|
const unattendedOn = value?.unattendedEnabled ?? false;
|
|
4441
4441
|
const diagramOn = value?.diagramEnabled ?? false;
|
|
4442
|
+
const acpFaceOn = value?.acpFaceEnabled ?? false;
|
|
4442
4443
|
const [openGateway, setOpenGateway] = (0, react.useState)(false);
|
|
4443
4444
|
const [openRebuild, setOpenRebuild] = (0, react.useState)(false);
|
|
4444
4445
|
const [openSkiff, setOpenSkiff] = (0, react.useState)(false);
|
|
4445
4446
|
const [openCro, setOpenCro] = (0, react.useState)(false);
|
|
4446
4447
|
const [openUnattended, setOpenUnattended] = (0, react.useState)(false);
|
|
4447
4448
|
const [openDiagram, setOpenDiagram] = (0, react.useState)(false);
|
|
4449
|
+
const [openAcpFace, setOpenAcpFace] = (0, react.useState)(false);
|
|
4448
4450
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4449
4451
|
className: "ss-section",
|
|
4450
4452
|
children: [
|
|
@@ -4578,9 +4580,26 @@ Instruction-following style:
|
|
|
4578
4580
|
})
|
|
4579
4581
|
}) })]
|
|
4580
4582
|
}),
|
|
4581
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
4583
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Group, {
|
|
4582
4584
|
title: "工具",
|
|
4583
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
|
|
4585
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
|
|
4586
|
+
title: "ACP 面(官方 ACP,端口 3200)",
|
|
4587
|
+
desc: "让标准 ACP 客户端(如 Agmente)连进本机;每 CCC 一枚 token 把关(默认关)",
|
|
4588
|
+
expandable: true,
|
|
4589
|
+
open: openAcpFace,
|
|
4590
|
+
onToggle: setOpenAcpFace,
|
|
4591
|
+
control: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Toggle, {
|
|
4592
|
+
checked: acpFaceOn,
|
|
4593
|
+
onChange: (on) => toggle("acpFaceEnabled", on)
|
|
4594
|
+
}),
|
|
4595
|
+
detail: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4596
|
+
className: "ss-detailStack",
|
|
4597
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4598
|
+
className: "ss-detailIntro",
|
|
4599
|
+
children: "ACP 面(面 E,2026-10-17 owner 令)\n· 作用:把**官方** @deepseek-ai/dsh-acp 挂进本进程,标准 ACP 客户端经 WebSocket 登入本机\n· 与「微信桥」的关系:两者是**入站通道的两个实现**,每个 CCC 二选一(在各自的\n .opencode/serenity.json 里配 im.channel)。本开关只管**端口开不开**,不管哪个 CCC 用哪条\n· 🔴 认证:本面绑 0.0.0.0 且 ACP 协议自身**不做认证** ⇒ 由 ACC 在握手前用\n **每 CCC 一枚 token**(localstore 键 IM_ACP_TOKEN)把关;没配 token 的 CCC 一律连不上\n· 怎么用:① 在目标 CCC 配 im.channel=acp + im.acp.role=<角色名> ② 生成 token\n ③ 客户端连 ws://<本机IP>:3200,带 Authorization: Bearer <token> 或 ?token=<token>\n· ⚠️ 明文 ws:// ⇒ 局域网内可抓包;**上公网必须走 wss + Cloudflare Access**\n· 协议无认证这一条由 ACC 补齐:未过 token 闸的连接**根本升不上来**(不是\"升上来再拒\")"
|
|
4600
|
+
})
|
|
4601
|
+
})
|
|
4602
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCard, {
|
|
4584
4603
|
title: "图(diagram)",
|
|
4585
4604
|
desc: "agent 给一段标签式源码,进程内直接画出 PNG 并贴进对话(不借浏览器;默认关)",
|
|
4586
4605
|
expandable: true,
|
|
@@ -4597,7 +4616,7 @@ Instruction-following style:
|
|
|
4597
4616
|
children: "图(diagram 工具)\n· 作用:agent 给一段**标签式源码**(<diagram> 根 + <rect>/<arrow>/<text>/<color>/<legend>),\n 由 ACC **在进程内**画成图并贴进对话 —— 不启浏览器、不截图\n· 产物:图进对话记录(可回看、可被后续轮次引用、可转微信);同时落两份文件在容器的\n _tmp/diagram/(.svg 可读可改 / .png 是进对话的那份)\n· 自带体检:重叠块 / 标签溢出 / 悬空端点 / 线压字,四条**只告警不拦图**;\n 写错的图元被跳过并逐条报错,其余照画\n· 中文能画:字体**随插件内置**(约 3.8 MB)—— 光栅化引擎看不见系统字体,只能喂内置的\n· 默认关:它是 owner 要的 demo 功能;**关着的时候工具根本不注册**(模型看不到它,不占上下文)\n· 图要贴进对话有个前提:**当前模型路由要声明收图像输入**。没声明时图照样画、照样落盘,\n 回执里写明原因并给路径(同「读图」工具的门)"
|
|
4598
4617
|
})
|
|
4599
4618
|
})
|
|
4600
|
-
}) })
|
|
4619
|
+
}) })]
|
|
4601
4620
|
}),
|
|
4602
4621
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Collapse, {
|
|
4603
4622
|
title: "彩蛋模式",
|
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
|
2
2
|
import { c as hostInjected, d as hostSessions, f as hostSettings, h as hostWebServer, i as cccRootForExec, l as hostLlm, m as hostWeb, n as agentCwdFor, o as listCccs, p as hostSubagents, r as cccRootForCwd, s as hostAgents, t as NO_CCC_FROM_AGENT_CWD, u as hostService } from "./ccc-roots-GIJ3A_tw.js";
|
|
3
3
|
import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readExclusiveTools, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-B0ZGL9lx.js";
|
|
4
4
|
import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-B4P3Uw6D.js";
|
|
5
|
-
import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as
|
|
5
|
+
import { _ as runLocalStore, a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as readStore, h as readGitTrack, i as matchWeixinRoute, l as weixinSessionIdFor, m as localstorePath, n as extractWeixinText, o as readWeixinSettings, p as getEntry, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia, v as writeEntry } from "./weixin-route-CSDBGLwl.js";
|
|
6
6
|
import { a as FACE_PORTS, c as MECHANISM_PORTS, d as registerSkiffSession$1, f as skiffRoleFor$1, h as ACC_VERSION, i as ACP_PORT, l as SKIFF_DEBUG_PORT, m as unregisterSkiffSession$1, n as readSimpleSettings, o as GATEWAY_PORT, p as skiffSessionInfo$1, r as registerSettingsSection, s as MAIN_WEB_PORT, u as WEIXIN_SEND_PORT } from "./settings-section-DGUQZo5j.js";
|
|
7
7
|
import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-B615L9MV.js";
|
|
8
8
|
import { REGISTERED_TOOLS } from "./invariant.js";
|
|
@@ -11273,7 +11273,10 @@ async function acquireWakeAgent(ctx, root, dirName) {
|
|
|
11273
11273
|
return { error: `冷会话载入失败(${id}): ${String(err?.message ?? err)}` };
|
|
11274
11274
|
}
|
|
11275
11275
|
}
|
|
11276
|
-
return {
|
|
11276
|
+
return {
|
|
11277
|
+
error: `无绑定会话记录(AGENT_SESSIONS/.bindings.json 中无 ${dirName})——目标轨迹需先被 container_trajectory use 激活过`,
|
|
11278
|
+
noBinding: true
|
|
11279
|
+
};
|
|
11277
11280
|
}
|
|
11278
11281
|
/** 唤醒消息(唤起即投递正文:身份锚定 + 唤醒信息 + 任务) */
|
|
11279
11282
|
function buildWakeText(entry, target) {
|
|
@@ -11395,11 +11398,21 @@ async function sendToTrajectory(ctx, root, target, message, sender) {
|
|
|
11395
11398
|
ok: false,
|
|
11396
11399
|
detail: `目标 trajectory 未命中(${target})`
|
|
11397
11400
|
};
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11401
|
+
let acquired = await acquireWakeAgent(ctx, root, t.dirName);
|
|
11402
|
+
let createdNote = "";
|
|
11403
|
+
if ("error" in acquired) {
|
|
11404
|
+
if (!acquired.notReady && acquired.noBinding === true) {
|
|
11405
|
+
acquired = await createAndBindTarget(ctx, root, t.dirName);
|
|
11406
|
+
if ("error" in acquired) return {
|
|
11407
|
+
ok: false,
|
|
11408
|
+
detail: acquired.error
|
|
11409
|
+
};
|
|
11410
|
+
createdNote = `;目标原本无活跃 dsh 会话 ⇒ **已自动创建并绑定**一条(${acquired.how})`;
|
|
11411
|
+
} else return {
|
|
11412
|
+
ok: false,
|
|
11413
|
+
detail: acquired.error
|
|
11414
|
+
};
|
|
11415
|
+
}
|
|
11403
11416
|
const agent = acquired.agent;
|
|
11404
11417
|
const make = () => createUserMessage({
|
|
11405
11418
|
content: [{
|
|
@@ -11419,14 +11432,14 @@ async function sendToTrajectory(ctx, root, target, message, sender) {
|
|
|
11419
11432
|
}
|
|
11420
11433
|
return {
|
|
11421
11434
|
ok: true,
|
|
11422
|
-
detail: `已即时投递 ${t.dirName}(${acquired.how};目标空闲 ⇒ followup
|
|
11435
|
+
detail: `已即时投递 ${t.dirName}(${acquired.how};目标空闲 ⇒ followup 立即起轮)${createdNote}`
|
|
11423
11436
|
};
|
|
11424
11437
|
}
|
|
11425
11438
|
try {
|
|
11426
11439
|
agent.steer(make());
|
|
11427
11440
|
return {
|
|
11428
11441
|
ok: true,
|
|
11429
|
-
detail: `已即时注入 ${t.dirName}(${acquired.how};目标在跑 ⇒ steer
|
|
11442
|
+
detail: `已即时注入 ${t.dirName}(${acquired.how};目标在跑 ⇒ steer 当场注入当前轮)${createdNote}`
|
|
11430
11443
|
};
|
|
11431
11444
|
} catch (err) {
|
|
11432
11445
|
try {
|
|
@@ -11439,11 +11452,72 @@ async function sendToTrajectory(ctx, root, target, message, sender) {
|
|
|
11439
11452
|
}
|
|
11440
11453
|
return {
|
|
11441
11454
|
ok: true,
|
|
11442
|
-
detail: `已投递 ${t.dirName}(${acquired.how};steer 不可用 ⇒ 退回 followup): ${String(err?.message ?? err)}`
|
|
11455
|
+
detail: `已投递 ${t.dirName}(${acquired.how};steer 不可用 ⇒ 退回 followup): ${String(err?.message ?? err)}${createdNote}`
|
|
11443
11456
|
};
|
|
11444
11457
|
}
|
|
11445
11458
|
}
|
|
11446
11459
|
/**
|
|
11460
|
+
* 🆕 **给一条「从未被激活过」的轨迹就地建一条 dsh 会话并绑定它**(2026-10-16 owner 令,S142)。
|
|
11461
|
+
*
|
|
11462
|
+
* 何时用:**只有** `send-now` 在 `acquireWakeAgent` 报「无绑定会话记录」时(见该处的判据说明)。
|
|
11463
|
+
*
|
|
11464
|
+
* 三步(顺序不可换):
|
|
11465
|
+
* ① `sessionController.create({ cwd: root })` —— 宿主原语,**idempotently adopt**
|
|
11466
|
+
* ⇒ 重复调用不会建出第二条(不必自造"先查再建"的竞态窗口)。
|
|
11467
|
+
* ② `resolveAgent(sessionId)` —— 拿到**真 agent**。`create` 只返回 id,不给 agent。
|
|
11468
|
+
* ③ `appendBound(...)` —— 落绑定。**必须由本函数写**:`listBoundSessionIds` 是
|
|
11469
|
+
* `acquireWakeAgent` 的唯一选人依据,不写绑定则下一条消息**还是**找不到它
|
|
11470
|
+
* (那就成了"建了但永远用不上",比不建更坏)。
|
|
11471
|
+
*
|
|
11472
|
+
* 🔴 **为什么用 `cwd: root` 而不是目标的目录**:会话的 cwd 决定它落在哪个 CCC
|
|
11473
|
+
* (`cccRootForCwd` 上溯 `.serenity`)。目标轨迹本就住在该 CCC 的 `AGENT_SESSIONS/` 下
|
|
11474
|
+
* ⇒ 传 CCC 根才让它**进同一个 CCC**;传轨迹目录会让它变成一个**不同 CCC**的会话。
|
|
11475
|
+
*
|
|
11476
|
+
* @returns 命中返回 agent 与取得方式;任一步失败返回**带原因**的 error(不静默、不误报成功)
|
|
11477
|
+
*/
|
|
11478
|
+
async function createAndBindTarget(ctx, root, dirName) {
|
|
11479
|
+
const sc = hostService(ctx, "sessionController");
|
|
11480
|
+
const create = sc?.create;
|
|
11481
|
+
if (typeof create !== "function") return {
|
|
11482
|
+
error: "目标无活跃 dsh 会话,且 sessionController.create 不可用(绑定 id: 无)——无法就地创建载体;请先对该轨迹执行一次 container_trajectory use",
|
|
11483
|
+
notReady: true
|
|
11484
|
+
};
|
|
11485
|
+
let sessionId = "";
|
|
11486
|
+
try {
|
|
11487
|
+
const out = await create.call(sc, { cwd: root });
|
|
11488
|
+
sessionId = String(out?.sessionId ?? "");
|
|
11489
|
+
} catch (err) {
|
|
11490
|
+
return { error: `就地创建会话失败(${dirName}): ${String(err?.message ?? err)}` };
|
|
11491
|
+
}
|
|
11492
|
+
if (!sessionId) return { error: `就地创建会话未返回 sessionId(${dirName})` };
|
|
11493
|
+
const resolved = sc?.resolveAgent;
|
|
11494
|
+
if (typeof resolved !== "function") return {
|
|
11495
|
+
error: `会话已建(${sessionId})但 sessionController.resolveAgent 不可用`,
|
|
11496
|
+
notReady: true
|
|
11497
|
+
};
|
|
11498
|
+
let agent;
|
|
11499
|
+
try {
|
|
11500
|
+
const out = await resolved.call(sc, sessionId);
|
|
11501
|
+
agent = out?.agent;
|
|
11502
|
+
if (!agent) return { error: `会话已建(${sessionId})但 resolveAgent 未返回 agent: ${String(out?.error ?? "未知原因")}` };
|
|
11503
|
+
} catch (err) {
|
|
11504
|
+
return { error: `会话已建(${sessionId})但载入失败: ${String(err?.message ?? err)}` };
|
|
11505
|
+
}
|
|
11506
|
+
const bound = appendBound({ header: {
|
|
11507
|
+
id: sessionId,
|
|
11508
|
+
cwd: root
|
|
11509
|
+
} }, "activate", {
|
|
11510
|
+
dirName,
|
|
11511
|
+
mdPath: join(root, "AGENT_SESSIONS", dirName, "SESSION.md"),
|
|
11512
|
+
note: "auto-created by send-now"
|
|
11513
|
+
});
|
|
11514
|
+
const how = `auto-create(${sessionId}${bound ? ", bound" : ", ⚠️ 绑定写入失败"})`;
|
|
11515
|
+
return {
|
|
11516
|
+
agent,
|
|
11517
|
+
how
|
|
11518
|
+
};
|
|
11519
|
+
}
|
|
11520
|
+
/**
|
|
11447
11521
|
* 本 tick 要扫的 CCC 根集合(**并集**)。
|
|
11448
11522
|
*
|
|
11449
11523
|
* C2(S142 2026-09-15 Q5):原实现是 `collectLiveCccs`——**只认 live 会话** ⇒
|
|
@@ -12083,13 +12157,56 @@ function renderText$3(value) {
|
|
|
12083
12157
|
function createTrajectoryTool(ctx) {
|
|
12084
12158
|
return defineTool({
|
|
12085
12159
|
name: "container_trajectory",
|
|
12086
|
-
description:
|
|
12160
|
+
description: [
|
|
12161
|
+
"container_trajectory — one trajectory: its persistent body (SESSION.md) + its timeline.",
|
|
12162
|
+
"A trajectory is NOT the conversation: SESSION.md is the durable body and never moves;",
|
|
12163
|
+
"the dsh conversation is only its rebuildable carrier (Ship of Theseus — the carrier is replaced, the trajectory continues).",
|
|
12164
|
+
"ACTIONS, grouped by EFFECT (read the grouping before picking one):",
|
|
12165
|
+
"(A) LIFECYCLE — changes which trajectory THIS conversation is bound to:",
|
|
12166
|
+
"list (inventory + stats + anomaly marks) / show (read one SESSION.md) / create (make a NEW trajectory) /",
|
|
12167
|
+
"use (bind THIS conversation to an existing trajectory; inline integrity check — silent when fine, problems reported as hints, never errors) /",
|
|
12168
|
+
"rebuild (clear-and-rebuild THIS conversation in place).",
|
|
12169
|
+
"(B) DELIVERY — sends ONE message to ANOTHER trajectory; your own binding is unchanged:",
|
|
12170
|
+
"send-now (deliver RIGHT NOW) / send-later (deliver at ONE future instant).",
|
|
12171
|
+
"(C) REFERENCE — cro-guide (how to give THIS trajectory its own program that decides when it should be woken).",
|
|
12172
|
+
"HOW TO CHOOSE (a rule, not a guess):",
|
|
12173
|
+
"not yet bound + starting new work ⇒ create — with no prior binding it activates immediately, so the new trajectory has a carrier at once.",
|
|
12174
|
+
"already bound + recording a separate future line ⇒ create — it does NOT steal your binding (you stay on the current trajectory);",
|
|
12175
|
+
"activate the new one when you want it by sending it a message.",
|
|
12176
|
+
"want THIS conversation to move to another trajectory ⇒ use (refuses when it would orphan the current binding; --force overrides).",
|
|
12177
|
+
"want to poke another trajectory ⇒ send-now. Its target needs NO prior setup:",
|
|
12178
|
+
"if the target has no live dsh session, one is created and bound automatically, and your message is injected into it.",
|
|
12179
|
+
"RECEIPTS (read no more than is written): send-now returns a synchronous receipt meaning exactly \"injected / turn started\" —",
|
|
12180
|
+
"NEVER \"the target ran or replied\"; to prove the target actually moved, read its SESSION.md.",
|
|
12181
|
+
"It never queues behind the current turn: a busy target is steered mid-turn (read in the CURRENT turn), an idle target starts a turn at once.",
|
|
12182
|
+
"send-later is fire-and-forget by design: no receipt, no recall, no panel.",
|
|
12183
|
+
"ASYMMETRY (deliberate — the two differ in whether a missing target is normal): send-now creates a target on demand,",
|
|
12184
|
+
"because sending to a never-used trajectory is a legitimate intent; send-later does NOT,",
|
|
12185
|
+
"because scheduling for a trajectory that has never existed is almost always a mistyped name — auto-creating would hide that error behind an empty trajectory.",
|
|
12186
|
+
"PARAMETERS: create requires --desc <desc> [--goal] or --issue <ticket> (exactly one) plus --summary (≤20 chars).",
|
|
12187
|
+
"use requires --summary (≤20 chars) and may pass --force to switch away from the currently-bound trajectory.",
|
|
12188
|
+
"rebuild requires --summary (next-phase summary ≤20 chars) + optional --note (task focus for the rebuilt self).",
|
|
12189
|
+
"send-later requires target (S### or AGENT_SESSIONS dir name) + at (RFC3339 or +30m/+2h) + message.",
|
|
12190
|
+
"send-now requires target + message.",
|
|
12191
|
+
"The summary is appended to the dsh session title (S###-YYYY-MM-DD-<summary>); the S### id and date stay server-derived."
|
|
12192
|
+
].join(" "),
|
|
12087
12193
|
parameters: {
|
|
12088
12194
|
action: {
|
|
12089
12195
|
type: "string",
|
|
12090
12196
|
enum: [...TRAJECTORY_ACTIONS],
|
|
12091
12197
|
required: true,
|
|
12092
|
-
description:
|
|
12198
|
+
description: [
|
|
12199
|
+
"Subcommand — grouped by effect.",
|
|
12200
|
+
"LIFECYCLE (changes THIS conversation's binding):",
|
|
12201
|
+
"list (inventory + stats + anomaly marks) / show (S### or dir name or fuzzy keyword) /",
|
|
12202
|
+
"create (new trajectory; activates itself when you have no binding, otherwise leaves your binding untouched and tells you how to activate it) /",
|
|
12203
|
+
"use (activate context for THIS conversation; inline integrity check — pass silent, problems reported as hints, not errors) /",
|
|
12204
|
+
"rebuild (clear-and-rebuild current conversation — requires --summary + optional --note).",
|
|
12205
|
+
"DELIVERY (sends one message to ANOTHER trajectory; your binding is unchanged):",
|
|
12206
|
+
"send-now (deliver RIGHT NOW; the target needs no prior setup — if it has no live session, one is created and bound automatically) /",
|
|
12207
|
+
"send-later (one future instant + one message; fire-and-forget, and it does NOT auto-create a target).",
|
|
12208
|
+
"REFERENCE: cro-guide (no parameters: prints the CRO programming guide, including a ready-to-use sample snapshot)"
|
|
12209
|
+
].join(" ")
|
|
12093
12210
|
},
|
|
12094
12211
|
name: {
|
|
12095
12212
|
type: "string",
|
|
@@ -12101,15 +12218,15 @@ function createTrajectoryTool(ctx) {
|
|
|
12101
12218
|
},
|
|
12102
12219
|
desc: {
|
|
12103
12220
|
type: "string",
|
|
12104
|
-
description: "create short description (any language, ≤5 words; mutually exclusive with issue)"
|
|
12221
|
+
description: "create: short description (any language, ≤5 words; mutually exclusive with issue)"
|
|
12105
12222
|
},
|
|
12106
12223
|
issue: {
|
|
12107
12224
|
type: "string",
|
|
12108
|
-
description: "create ticket number (e.g. apaas-26116; dir named YYYY-MM-DD--<issue>; mutually exclusive with desc)"
|
|
12225
|
+
description: "create: ticket number (e.g. apaas-26116; dir named YYYY-MM-DD--<issue>; mutually exclusive with desc)"
|
|
12109
12226
|
},
|
|
12110
12227
|
goal: {
|
|
12111
12228
|
type: "string",
|
|
12112
|
-
description: "create one-sentence goal (optional)"
|
|
12229
|
+
description: "create: one-sentence goal (optional)"
|
|
12113
12230
|
},
|
|
12114
12231
|
summary: {
|
|
12115
12232
|
type: "string",
|
|
@@ -12121,7 +12238,7 @@ function createTrajectoryTool(ctx) {
|
|
|
12121
12238
|
},
|
|
12122
12239
|
dryRun: {
|
|
12123
12240
|
type: "boolean",
|
|
12124
|
-
description: "create preview mode (no actual changes)"
|
|
12241
|
+
description: "create: preview mode (no actual changes)"
|
|
12125
12242
|
},
|
|
12126
12243
|
target: {
|
|
12127
12244
|
type: "string",
|
|
@@ -12160,13 +12277,32 @@ function createTrajectoryTool(ctx) {
|
|
|
12160
12277
|
dryRun: isDryRun
|
|
12161
12278
|
});
|
|
12162
12279
|
const message = result.message;
|
|
12163
|
-
if (
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
|
|
12169
|
-
|
|
12280
|
+
if (isDryRun) return message;
|
|
12281
|
+
const scope = agentScope$1(exec);
|
|
12282
|
+
const priorBound = currentBoundDirName(exec, scope);
|
|
12283
|
+
if (priorBound === null) {
|
|
12284
|
+
const active = useSession(root, result.dirName, scope);
|
|
12285
|
+
const info = getActiveSessionInfo(scope);
|
|
12286
|
+
if (info) renameDshSessionForActive(ctx, exec, info, args.summary);
|
|
12287
|
+
const dsh = agentDshSession(exec);
|
|
12288
|
+
if (dsh && info) appendBound(dsh, "activate", {
|
|
12289
|
+
dirName: info.dirName,
|
|
12290
|
+
mdPath: info.mdPath,
|
|
12291
|
+
sessionId: info.sessionId,
|
|
12292
|
+
note: "auto-activated on create (no prior binding)"
|
|
12293
|
+
});
|
|
12294
|
+
return [
|
|
12295
|
+
message,
|
|
12296
|
+
`✓ 已自动激活(此前**无绑定**):${active.dir} —— 后续工作写回该轨迹的 SESSION.md。`,
|
|
12297
|
+
` (判据:create 前无任何绑定 ⇒ 激活;有绑定则只建不夺,见下。)`
|
|
12298
|
+
].join("\n");
|
|
12299
|
+
}
|
|
12300
|
+
return [
|
|
12301
|
+
message,
|
|
12302
|
+
`⚠ 本轨迹【未】被激活 —— 当前已绑定 ${priorBound},保绑定优先(不夺走你正在用的轨迹)。`,
|
|
12303
|
+
` 要激活它(并在它名下开一条会话):用 container_trajectory send-now 向 ${result.dirName} 投一条消息 ——`,
|
|
12304
|
+
` 目标若没有活跃 dsh 会话,会自动创建一条并把「使用提示词 + 你的消息」一并注入。`
|
|
12305
|
+
].join("\n");
|
|
12170
12306
|
}
|
|
12171
12307
|
case "use": {
|
|
12172
12308
|
if (!args.name) throw new Error("use requires name (S### or directory name)");
|
|
@@ -15187,11 +15323,11 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15187
15323
|
sendJson$3(res, 400, { error: chk.error });
|
|
15188
15324
|
return;
|
|
15189
15325
|
}
|
|
15190
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
15326
|
+
const { readWeixinSettings } = await import("./weixin-route-CSDBGLwl.js").then((n) => n.u);
|
|
15191
15327
|
const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15192
15328
|
const settings = readWeixinSettings(chk.root);
|
|
15193
15329
|
const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
|
|
15194
|
-
const { readWeixinCredential } = await import("./weixin-route-
|
|
15330
|
+
const { readWeixinCredential } = await import("./weixin-route-CSDBGLwl.js").then((n) => n.u);
|
|
15195
15331
|
const accounts = (settings.accounts ?? []).map((a) => ({
|
|
15196
15332
|
accountId: a.accountId,
|
|
15197
15333
|
name: a.name ?? void 0,
|
|
@@ -15223,7 +15359,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15223
15359
|
const { fetchQRCode } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
|
|
15224
15360
|
const { randomUUID } = await import("node:crypto");
|
|
15225
15361
|
for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
|
|
15226
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
15362
|
+
const { readWeixinSettings } = await import("./weixin-route-CSDBGLwl.js").then((n) => n.u);
|
|
15227
15363
|
const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
|
|
15228
15364
|
const loginKey = randomUUID();
|
|
15229
15365
|
weixinLogins.set(loginKey, {
|
|
@@ -15244,7 +15380,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15244
15380
|
sendJson$3(res, 400, { error: "missing accountId" });
|
|
15245
15381
|
return;
|
|
15246
15382
|
}
|
|
15247
|
-
const { removeWeixinAccount } = await import("./weixin-route-
|
|
15383
|
+
const { removeWeixinAccount } = await import("./weixin-route-CSDBGLwl.js").then((n) => n.u);
|
|
15248
15384
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15249
15385
|
removeWeixinAccount(root, body.accountId);
|
|
15250
15386
|
syncCccBridge(ctx, root);
|
|
@@ -15257,7 +15393,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15257
15393
|
sendJson$3(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
|
|
15258
15394
|
return;
|
|
15259
15395
|
}
|
|
15260
|
-
const { saveWeixinRoutes } = await import("./weixin-route-
|
|
15396
|
+
const { saveWeixinRoutes } = await import("./weixin-route-CSDBGLwl.js").then((n) => n.u);
|
|
15261
15397
|
const { readSkiffRoles } = await import("./skiff-role-B615L9MV.js").then((n) => n.u);
|
|
15262
15398
|
const roles = readSkiffRoles(root);
|
|
15263
15399
|
for (const r of routes) if (!roles.has(r.role)) {
|
|
@@ -15270,7 +15406,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15270
15406
|
}
|
|
15271
15407
|
if (body.action === "set-enabled") {
|
|
15272
15408
|
const enabled = body.enabled === true;
|
|
15273
|
-
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-
|
|
15409
|
+
const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-CSDBGLwl.js").then((n) => n.u);
|
|
15274
15410
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15275
15411
|
const settings = readWeixinSettings(root);
|
|
15276
15412
|
if (enabled && (settings.accounts ?? []).length === 0) {
|
|
@@ -15314,7 +15450,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15314
15450
|
return;
|
|
15315
15451
|
}
|
|
15316
15452
|
const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
|
|
15317
|
-
const { readWeixinSettings } = await import("./weixin-route-
|
|
15453
|
+
const { readWeixinSettings } = await import("./weixin-route-CSDBGLwl.js").then((n) => n.u);
|
|
15318
15454
|
const settings = readWeixinSettings(login.root);
|
|
15319
15455
|
const status = await pollQRStatus({
|
|
15320
15456
|
baseUrl: void 0,
|
|
@@ -15329,7 +15465,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
15329
15465
|
});
|
|
15330
15466
|
return;
|
|
15331
15467
|
}
|
|
15332
|
-
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-
|
|
15468
|
+
const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-CSDBGLwl.js").then((n) => n.u);
|
|
15333
15469
|
const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
|
|
15334
15470
|
const accountId = nextWeixinAccountId(settings);
|
|
15335
15471
|
upsertWeixinAccount(login.root, {
|
|
@@ -21680,9 +21816,325 @@ function registerWeixinSendApi(ctx) {
|
|
|
21680
21816
|
sync();
|
|
21681
21817
|
}
|
|
21682
21818
|
//#endregion
|
|
21819
|
+
//#region src/acp-auth.ts
|
|
21820
|
+
/**
|
|
21821
|
+
* acp-auth.ts — ACP 面的**认证 + 身份**层(v1.59.0,owner 2026-10-17 令)
|
|
21822
|
+
*
|
|
21823
|
+
* ## 它解决什么
|
|
21824
|
+
*
|
|
21825
|
+
* owner 令(逐字):「将微信桥升级为 IM 桥,每个 CCC 可以选择微信实现或 ACP 实现,
|
|
21826
|
+
* 微信实现保留不变向前兼容,**ACP 实现就可以暴露端口并绑定 skiff,并且要随即生成认证用的 token**」。
|
|
21827
|
+
*
|
|
21828
|
+
* 面 E(`acp-face.ts`)原本**无认证、无 CCC 归属、无角色** —— 本模块补上这三件:
|
|
21829
|
+
* ① **token**:每 CCC 一枚,落**该 CCC 的 localstore**(credential scope)。
|
|
21830
|
+
* ② **CCC 归属**:连接建立时由 token 反查 ⇒ **服务端权威**(不拿客户端自述的 `cwd` 当身份)。
|
|
21831
|
+
* ③ **角色**:该 CCC 的 `im.acp.role` ⇒ 供装配层把 ACP 会话套上 skiff 角色。
|
|
21832
|
+
*
|
|
21833
|
+
* ## 🔴 架构决策:一面多 token,不是每 CCC 一个端口
|
|
21834
|
+
*
|
|
21835
|
+
* | | 甲:每 CCC 一个端口 | **乙:一面多 token** ← 选它 |
|
|
21836
|
+
* |---|---|---|
|
|
21837
|
+
* | 端口 | 随 CCC 数增长 | **固定 3200** |
|
|
21838
|
+
* | 身份来源 | 端口号 | **token(一一对应 CCC)** |
|
|
21839
|
+
*
|
|
21840
|
+
* **判据**:`token → CCC` 是一一对应,而 `cwd` 是**客户端可填的**。
|
|
21841
|
+
* ⇒ 用 token 定 CCC 比用 cwd 定 CCC 强:前者**服务端权威**,后者**客户端自述**。
|
|
21842
|
+
* ⇒ 连接一建立就知道"这是哪个 CCC",**该连接的所有会话都归它**,角色绑定无需猜。
|
|
21843
|
+
* ⚠️ `cwd` 仍要校验(见 `cwdInsideRoot`):防一枚 token 越权到别的 CCC。
|
|
21844
|
+
*
|
|
21845
|
+
* ## 🔴 为什么认证放在 WS upgrade 层,而不在 ACP 协议里
|
|
21846
|
+
*
|
|
21847
|
+
* `dsh-acp` 的 `authenticate()` **恒成功**且 `authMethods: []`(读其 `lib/index.js` 逐字),
|
|
21848
|
+
* 且它**不导出**可替换的认证缝 ⇒ 改协议层要重写它的接线(P0′ 已实测此路不通)。
|
|
21849
|
+
* 而**我们自己持有 HTTP server**(自持面)⇒ 在 `upgrade` **升级之前**校验,
|
|
21850
|
+
* 失败即 `401 + destroy` ⇒ **连接根本进不来**。
|
|
21851
|
+
*
|
|
21852
|
+
* ## 边界
|
|
21853
|
+
*
|
|
21854
|
+
* 本模块**零 DSH 依赖**(只 `node:crypto` / `node:fs` / `node:path` 与本仓纯逻辑)⇒ 可独立单测。
|
|
21855
|
+
*/
|
|
21856
|
+
/** localstore 凭据键(credential scope ⇒ UPPER_SNAKE) */
|
|
21857
|
+
const ACP_TOKEN_KEY = "IM_ACP_TOKEN";
|
|
21858
|
+
/** token 字节数(32 B = 256 bit;hex 后 64 字符) */
|
|
21859
|
+
const TOKEN_BYTES = 32;
|
|
21860
|
+
/**
|
|
21861
|
+
* 该 CCC 的 ACP token(**读**;不存在返回 null)。
|
|
21862
|
+
*
|
|
21863
|
+
* 🔴 **不自动生成** —— 生成是**写操作**,只在 owner/面板显式要求时做
|
|
21864
|
+
* (`ensureAcpToken`)。读路径永远无副作用:否则每次启动都会改 localstore。
|
|
21865
|
+
*/
|
|
21866
|
+
function readAcpToken(root) {
|
|
21867
|
+
try {
|
|
21868
|
+
const value = getEntry(root, "credential", ACP_TOKEN_KEY);
|
|
21869
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
21870
|
+
} catch {
|
|
21871
|
+
return null;
|
|
21872
|
+
}
|
|
21873
|
+
}
|
|
21874
|
+
/**
|
|
21875
|
+
* 生成(或返回既有)ACP token 并落该 CCC 的 localstore。
|
|
21876
|
+
*
|
|
21877
|
+
* ⚠️ **幂等**:已有 token 时**原样返回**(不轮换)—— 轮换会让在用的客户端立刻断线,
|
|
21878
|
+
* 必须是一次显式动作(`resetAcpToken`)。
|
|
21879
|
+
*/
|
|
21880
|
+
function ensureAcpToken(root) {
|
|
21881
|
+
const existing = readAcpToken(root);
|
|
21882
|
+
if (existing) return existing;
|
|
21883
|
+
const token = randomBytes(TOKEN_BYTES).toString("hex");
|
|
21884
|
+
writeEntry(root, "credential", ACP_TOKEN_KEY, token);
|
|
21885
|
+
return token;
|
|
21886
|
+
}
|
|
21887
|
+
/**
|
|
21888
|
+
* 常时比较(防时序侧信道)。
|
|
21889
|
+
*
|
|
21890
|
+
* 🔴 两个长度不等时 `timingSafeEqual` **会抛** ⇒ 先比长度并立刻返回 false。
|
|
21891
|
+
* ⚠️ 那一步本身**泄漏长度**(可接受:token 长度固定且公开)。
|
|
21892
|
+
*/
|
|
21893
|
+
function safeEqual(a, b) {
|
|
21894
|
+
const bufA = Buffer.from(a, "utf8");
|
|
21895
|
+
const bufB = Buffer.from(b, "utf8");
|
|
21896
|
+
if (bufA.length !== bufB.length) return false;
|
|
21897
|
+
return timingSafeEqual(bufA, bufB);
|
|
21898
|
+
}
|
|
21899
|
+
/** token 指纹(诊断面用;不可逆) */
|
|
21900
|
+
function tokenFingerprint(token) {
|
|
21901
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
21902
|
+
}
|
|
21903
|
+
/**
|
|
21904
|
+
* 从一次 HTTP upgrade 请求里取出候选 token。
|
|
21905
|
+
*
|
|
21906
|
+
* 两种携带方式都认(客户端差异;Agmente 等用具名头,脚本常用查询串):
|
|
21907
|
+
* ① `Authorization: Bearer <t>`
|
|
21908
|
+
* ② `?token=<t>`(查询串)
|
|
21909
|
+
*
|
|
21910
|
+
* @returns 取到的 token;两者都无 → null
|
|
21911
|
+
*/
|
|
21912
|
+
function extractToken(headers, url) {
|
|
21913
|
+
const auth = headers["authorization"];
|
|
21914
|
+
if (typeof auth === "string") {
|
|
21915
|
+
const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
|
|
21916
|
+
if (m?.[1]) return m[1].trim();
|
|
21917
|
+
}
|
|
21918
|
+
if (typeof url === "string" && url !== "") try {
|
|
21919
|
+
const q = new URL(url, "http://placeholder.invalid").searchParams.get("token");
|
|
21920
|
+
if (q && q.trim() !== "") return q.trim();
|
|
21921
|
+
} catch {}
|
|
21922
|
+
return null;
|
|
21923
|
+
}
|
|
21924
|
+
/**
|
|
21925
|
+
* 校验一枚候选 token,返回它归属的 CCC 根(**未命中返回 null**)。
|
|
21926
|
+
*
|
|
21927
|
+
* @param candidate 客户端出示的 token
|
|
21928
|
+
* @param roots 候选 CCC 根集合(由调用方枚举——本模块不认识"CCC 在哪")
|
|
21929
|
+
*/
|
|
21930
|
+
function resolveTokenOwner(candidate, roots) {
|
|
21931
|
+
if (!candidate) return null;
|
|
21932
|
+
for (const root of roots) {
|
|
21933
|
+
const token = readAcpToken(root);
|
|
21934
|
+
if (token && safeEqual(candidate, token)) return root;
|
|
21935
|
+
}
|
|
21936
|
+
return null;
|
|
21937
|
+
}
|
|
21938
|
+
/**
|
|
21939
|
+
* 枚举**启用了 ACP 通道**的 CCC(供 `resolveTokenOwner` 用)。
|
|
21940
|
+
*
|
|
21941
|
+
* 判据(与装配层同源,见 `acpChannelEnabled`):`im.channel === 'acp'` ∧ `im.acp.enabled === true`
|
|
21942
|
+
* ∧ token 已生成。⚠️ 三者**缺一即不算**(fail-closed:宁可连不上,不可放行一个半配置的通道)。
|
|
21943
|
+
*/
|
|
21944
|
+
function listAcpRoots(roots, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
21945
|
+
const out = [];
|
|
21946
|
+
for (const root of roots) {
|
|
21947
|
+
if (!acpChannelEnabled(root, paths)) continue;
|
|
21948
|
+
if (!readAcpToken(root)) continue;
|
|
21949
|
+
out.push(root);
|
|
21950
|
+
}
|
|
21951
|
+
return out;
|
|
21952
|
+
}
|
|
21953
|
+
/** 读该 CCC 的 `im` 段(未配置 ⇒ 归一缺省;永不 undefined) */
|
|
21954
|
+
function readImSettings(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
21955
|
+
try {
|
|
21956
|
+
const im = loadSerenityConfig(root, paths).im;
|
|
21957
|
+
if (!im || typeof im !== "object") return {
|
|
21958
|
+
channel: "weixin",
|
|
21959
|
+
acp: {}
|
|
21960
|
+
};
|
|
21961
|
+
return {
|
|
21962
|
+
channel: im.channel === "acp" ? "acp" : "weixin",
|
|
21963
|
+
acp: im.acp && typeof im.acp === "object" ? im.acp : {}
|
|
21964
|
+
};
|
|
21965
|
+
} catch {
|
|
21966
|
+
return {
|
|
21967
|
+
channel: "weixin",
|
|
21968
|
+
acp: {}
|
|
21969
|
+
};
|
|
21970
|
+
}
|
|
21971
|
+
}
|
|
21972
|
+
/**
|
|
21973
|
+
* 该 CCC 是否启用 ACP 入站通道。
|
|
21974
|
+
*
|
|
21975
|
+
* 🔴 **两个条件都真才算**:`im.channel === 'acp'` **且** `im.acp.enabled === true`。
|
|
21976
|
+
* 为什么双键:`channel` 表达"选了哪条路",`enabled` 表达"现在开不开" ⇒
|
|
21977
|
+
* 让 owner 能**选好 ACP 但暂时关着**(不用删配置),与既有各面的 `enabled` 范式一致。
|
|
21978
|
+
*/
|
|
21979
|
+
function acpChannelEnabled(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
21980
|
+
const im = readImSettings(root, paths);
|
|
21981
|
+
return im.channel === "acp" && im.acp.enabled === true;
|
|
21982
|
+
}
|
|
21983
|
+
/** 该 CCC 绑定的 skiff 角色名(未配置 ⇒ null) */
|
|
21984
|
+
function acpRoleFor(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
21985
|
+
const role = readImSettings(root, paths).acp.role;
|
|
21986
|
+
return typeof role === "string" && role.trim() !== "" ? role.trim() : null;
|
|
21987
|
+
}
|
|
21988
|
+
/**
|
|
21989
|
+
* 客户端 `session/new` 传来的 `cwd` 是否落在该 token 归属的 CCC 内。
|
|
21990
|
+
*
|
|
21991
|
+
* 🔴 **为什么必须校验**:token 定的是"哪个 CCC",而 `cwd` 由**客户端**指定。
|
|
21992
|
+
* 不校验 ⇒ 一枚 CCC A 的 token 可以把会话开到 CCC B(越权读到另一个 CCC 的上下文)。
|
|
21993
|
+
* 这与 CCC 自身的路径围墙(P3)是同一条纪律,只是发生在**接入**这一层。
|
|
21994
|
+
*
|
|
21995
|
+
* 判据用 `relative()` 而非 `startsWith`(后者会把 `/a/bc` 误判为在 `/a/b` 内)。
|
|
21996
|
+
*/
|
|
21997
|
+
function cwdInsideRoot(cwd, root) {
|
|
21998
|
+
if (!isAbsolute(cwd)) return false;
|
|
21999
|
+
const rel = relative(resolve(root), resolve(cwd));
|
|
22000
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
22001
|
+
}
|
|
22002
|
+
//#endregion
|
|
21683
22003
|
//#region src/acp-face.ts
|
|
21684
22004
|
/** 面名(面宿主幂等键;与 FACE_PORTS 的 id 同名,便于对账) */
|
|
21685
22005
|
const FACE_ACP = "acp";
|
|
22006
|
+
/**
|
|
22007
|
+
* 校验一次 upgrade 请求(**只读**:不改任何状态)。
|
|
22008
|
+
*
|
|
22009
|
+
* 判据链(任一环失败即拒,fail-closed):候选 token 缺失 ⇒ 不匹配任何 CCC ⇒
|
|
22010
|
+
* 该 CCC 未启用 ACP 通道 ⇒ 该 CCC 未配角色。最后一环尤其重要:**没有角色的 ACP 会话
|
|
22011
|
+
* 等于一个无身份、无约束的裸 agent** ⇒ 宁可连不上。
|
|
22012
|
+
*/
|
|
22013
|
+
function authorizeUpgrade(req) {
|
|
22014
|
+
const candidate = extractToken(req.headers, req.url);
|
|
22015
|
+
if (!candidate) return {
|
|
22016
|
+
ok: false,
|
|
22017
|
+
reason: "未携带 token(用 Authorization: Bearer <t> 或 ?token=<t>)"
|
|
22018
|
+
};
|
|
22019
|
+
const roots = acpRoots;
|
|
22020
|
+
if (roots.length === 0) return {
|
|
22021
|
+
ok: false,
|
|
22022
|
+
reason: "本机没有任何启用了 ACP 通道的 CCC"
|
|
22023
|
+
};
|
|
22024
|
+
const owner = resolveTokenOwner(candidate, roots);
|
|
22025
|
+
if (!owner) return {
|
|
22026
|
+
ok: false,
|
|
22027
|
+
reason: "token 不匹配任何已启用 ACP 通道的 CCC"
|
|
22028
|
+
};
|
|
22029
|
+
const role = acpRoleFor(owner);
|
|
22030
|
+
if (!role) return {
|
|
22031
|
+
ok: false,
|
|
22032
|
+
reason: `CCC(${owner})未配置 im.acp.role`
|
|
22033
|
+
};
|
|
22034
|
+
return {
|
|
22035
|
+
ok: true,
|
|
22036
|
+
root: owner,
|
|
22037
|
+
role
|
|
22038
|
+
};
|
|
22039
|
+
}
|
|
22040
|
+
/**
|
|
22041
|
+
* 归属候选 CCC 的**同步**缓存。
|
|
22042
|
+
*
|
|
22043
|
+
* 🔴 为什么缓存而非每次现算:`listCccs()` 是 **async**(要读 workspace registry +
|
|
22044
|
+
* `sessionPersistence`),而 `'upgrade'` 回调是**同步**的 ⇒ 不能在那里 await。
|
|
22045
|
+
* ⇒ 启动时算一次 + 配置变更时刷新;这是**准入名单**,宁可用稍旧的名单(新 CCC 多等一次
|
|
22046
|
+
* 配置事件即可生效),也不让 upgrade 挂起。
|
|
22047
|
+
*/
|
|
22048
|
+
let acpRoots = [];
|
|
22049
|
+
/** 刷新准入名单(**只收启用了 ACP 通道且已生成 token 的 CCC**)。返回新名单长度。 */
|
|
22050
|
+
async function refreshAcpRoots(ctx) {
|
|
22051
|
+
try {
|
|
22052
|
+
const all = (await listCccs(ctx)).map((e) => e.root);
|
|
22053
|
+
for (const root of all) try {
|
|
22054
|
+
if (acpChannelEnabled(root) && !readAcpToken(root)) {
|
|
22055
|
+
const token = ensureAcpToken(root);
|
|
22056
|
+
console.log(`[serenity-hooks] ✓ 面 E:已为 CCC 生成 ACP token(${root},指纹 ${tokenFingerprint(token)})`);
|
|
22057
|
+
}
|
|
22058
|
+
} catch (err) {
|
|
22059
|
+
console.warn(`[serenity-hooks] 面 E:为 CCC 生成 token 失败(${root}): ${String(err?.message ?? err)}`);
|
|
22060
|
+
}
|
|
22061
|
+
acpRoots = listAcpRoots(all);
|
|
22062
|
+
} catch (err) {
|
|
22063
|
+
acpRoots = [];
|
|
22064
|
+
console.warn(`[serenity-hooks] 面 E:CCC 准入名单刷新失败(已清空,连接将被拒): ${String(err?.message ?? err)}`);
|
|
22065
|
+
}
|
|
22066
|
+
return acpRoots.length;
|
|
22067
|
+
}
|
|
22068
|
+
/**
|
|
22069
|
+
* 把**本连接归属的 CCC 角色**套到该连接创建的 agent 上(v1.59.0)。
|
|
22070
|
+
*
|
|
22071
|
+
* ## 🔴 为什么是"认领"而不是"直接挂"
|
|
22072
|
+
*
|
|
22073
|
+
* 理想形态是:在本连接的 scoped fiber 里监听 `session/created`,**只**收到本连接建的会话。
|
|
22074
|
+
* 但那条路依赖一个**我没有实测过的**引擎行为 —— cordis 的 `EventsService.dispatch()` 只在
|
|
22075
|
+
* **派发方设了 `Context.filter`** 时才按 ctx 过滤(读 `@deepseek-ai/cordis/src/events.ts`
|
|
22076
|
+
* 的 `.filter(hook => hook.global || !filter || filter.call(thisArg, hook.ctx))`)。
|
|
22077
|
+
* ⇒ 若宿主派发 `session/created` 时**没设**过滤器,本监听器会**收到全进程**的会话
|
|
22078
|
+
* (网页端、别的轨迹、别的连接),**套错角色**。
|
|
22079
|
+
*
|
|
22080
|
+
* 🔴 **D119 纪律**:不许照源码的"意图"断言引擎行为 ⇒ 本实现**不赌**那条语义,
|
|
22081
|
+
* 改用**认领制**(两条过滤都可以自证,不依赖 filter):
|
|
22082
|
+
* ① **cwd 必须落在本连接归属的 CCC 内**(token 已定 CCC ⇒ 越界会话直接跳过);
|
|
22083
|
+
* ② **同一会话只被认领一次**(`claimed` 集合;先到先得 ⇒ 不会被另一条连接的监听器抢走)。
|
|
22084
|
+
*
|
|
22085
|
+
* ⚠️ **残余风险(如实标注)**:若**同一 CCC** 同时有网页端会话与 ACP 会话在创建,
|
|
22086
|
+
* 本函数可能把网页端那个会话也认领了(两者 cwd 都在该 CCC 内)。⇒ 判据是"cwd + 先到先得",
|
|
22087
|
+
* **不是"确属本连接"**。⚠️ **这一格必须由探针实测**(见 REBUILD-TODO 的待办);
|
|
22088
|
+
* 未实测前**不得**声称"只套本连接的会话"。
|
|
22089
|
+
*
|
|
22090
|
+
* 失败语义:全程不抛(本面是可选面)——角色挂不上只记日志,不阻断 ACP 接线。
|
|
22091
|
+
*/
|
|
22092
|
+
function bindRoleToConnScopedAgent(scoped, root, roleName) {
|
|
22093
|
+
try {
|
|
22094
|
+
const role = readSkiffRoles(root).get(roleName);
|
|
22095
|
+
if (!role) {
|
|
22096
|
+
console.warn(`[serenity-hooks] 面 E:CCC(${root})未定义 skiff 角色 "${roleName}" ⇒ 本连接不注入角色`);
|
|
22097
|
+
return;
|
|
22098
|
+
}
|
|
22099
|
+
const rolePrompt = createRolePromptReader(root, role);
|
|
22100
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
22101
|
+
const onCreated = (session) => {
|
|
22102
|
+
try {
|
|
22103
|
+
const id = session?.header?.id;
|
|
22104
|
+
const cwd = session?.header?.cwd;
|
|
22105
|
+
if (!id || !cwd) return;
|
|
22106
|
+
if (claimed.has(id)) return;
|
|
22107
|
+
if (!cwdInsideRoot(cwd, root)) return;
|
|
22108
|
+
const target = resolveAgentById(scoped, id);
|
|
22109
|
+
if (!target) return;
|
|
22110
|
+
target.ctx.systemPrompt.section({
|
|
22111
|
+
name: "serenity-skiff",
|
|
22112
|
+
order: -60,
|
|
22113
|
+
text: () => [buildSkiffBasePrompt(roleName, role), rolePrompt()].filter(Boolean).join("\n")
|
|
22114
|
+
});
|
|
22115
|
+
claimed.add(id);
|
|
22116
|
+
console.log(`[serenity-hooks] ✓ 面 E:会话 ${id} 已绑定角色 "${roleName}"(CCC=${root})`);
|
|
22117
|
+
} catch (err) {
|
|
22118
|
+
console.warn(`[serenity-hooks] 面 E:角色注入失败(不影响会话): ${String(err?.message ?? err)}`);
|
|
22119
|
+
}
|
|
22120
|
+
};
|
|
22121
|
+
for (const eventName of ["session/created", "agent/created"]) try {
|
|
22122
|
+
scoped.on(eventName, onCreated);
|
|
22123
|
+
} catch {}
|
|
22124
|
+
} catch (err) {
|
|
22125
|
+
console.warn(`[serenity-hooks] 面 E:角色绑定装配失败: ${String(err?.message ?? err)}`);
|
|
22126
|
+
}
|
|
22127
|
+
}
|
|
22128
|
+
/** 按会话 id 取 live agent(只读;取不到返回 undefined) */
|
|
22129
|
+
function resolveAgentById(scoped, sessionId) {
|
|
22130
|
+
try {
|
|
22131
|
+
const agent = hostInjected(scoped, "agents")?.get?.(sessionId);
|
|
22132
|
+
if (!agent?.ctx?.systemPrompt || typeof agent.ctx.systemPrompt.section !== "function") return void 0;
|
|
22133
|
+
return agent;
|
|
22134
|
+
} catch {
|
|
22135
|
+
return;
|
|
22136
|
+
}
|
|
22137
|
+
}
|
|
21686
22138
|
/** 本面绑定的地址(取自集中端口表 —— **单一真相源**,不在本文件硬写) */
|
|
21687
22139
|
const ACP_HOST = FACE_PORTS.acp.host ?? "127.0.0.1";
|
|
21688
22140
|
/**
|
|
@@ -21756,6 +22208,7 @@ function newState() {
|
|
|
21756
22208
|
wss: null,
|
|
21757
22209
|
connections: 0,
|
|
21758
22210
|
applied: 0,
|
|
22211
|
+
rejected: 0,
|
|
21759
22212
|
lastErrors: [],
|
|
21760
22213
|
disposers: []
|
|
21761
22214
|
};
|
|
@@ -21802,9 +22255,33 @@ async function startAcpFace(ctx) {
|
|
|
21802
22255
|
local.handle = handle;
|
|
21803
22256
|
handle.server.on("upgrade", (req, socket, head) => {
|
|
21804
22257
|
local.connections += 1;
|
|
22258
|
+
const verdict = authorizeUpgrade(req);
|
|
22259
|
+
if (!verdict.ok) {
|
|
22260
|
+
local.rejected += 1;
|
|
22261
|
+
local.lastErrors.push({
|
|
22262
|
+
at: Date.now(),
|
|
22263
|
+
applied: false,
|
|
22264
|
+
error: `auth: ${verdict.reason}`
|
|
22265
|
+
});
|
|
22266
|
+
if (local.lastErrors.length > MAX_ERR_KEEP) local.lastErrors.shift();
|
|
22267
|
+
try {
|
|
22268
|
+
socket.write(`HTTP/1.1 401 Unauthorized\r
|
|
22269
|
+
content-type: text/plain; charset=utf-8\r
|
|
22270
|
+
connection: close\r
|
|
22271
|
+
\r
|
|
22272
|
+
ACP 面:认证失败(${verdict.reason})。请在该 CCC 的 localstore 生成并使用 IM_ACP_TOKEN。\n`);
|
|
22273
|
+
} catch {}
|
|
22274
|
+
try {
|
|
22275
|
+
socket.destroy();
|
|
22276
|
+
} catch {}
|
|
22277
|
+
console.warn(`[serenity-hooks] ✗ 面 E:拒绝一条连接(${verdict.reason})`);
|
|
22278
|
+
return;
|
|
22279
|
+
}
|
|
21805
22280
|
try {
|
|
21806
22281
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
21807
22282
|
try {
|
|
22283
|
+
const connRoot = verdict.root;
|
|
22284
|
+
const connRole = verdict.role;
|
|
21808
22285
|
const stream = wsToAcpStream(ws, (line) => {
|
|
21809
22286
|
console.log(`[serenity-hooks] 面 E:丢弃一条非 JSON 帧(前 120 字): ${line}`);
|
|
21810
22287
|
});
|
|
@@ -21812,6 +22289,7 @@ async function startAcpFace(ctx) {
|
|
|
21812
22289
|
inject: ACP_REQUIRED_SERVICES,
|
|
21813
22290
|
apply: (scoped) => {
|
|
21814
22291
|
const sel = resolveAcpSelection(scoped);
|
|
22292
|
+
bindRoleToConnScopedAgent(scoped, connRoot, connRole);
|
|
21815
22293
|
dshAcp.apply(scoped, {
|
|
21816
22294
|
stream,
|
|
21817
22295
|
provider: sel.provider,
|
|
@@ -21971,6 +22449,17 @@ function registerAcpFace(ctx) {
|
|
|
21971
22449
|
sync();
|
|
21972
22450
|
});
|
|
21973
22451
|
} catch {}
|
|
22452
|
+
const refresh = () => {
|
|
22453
|
+
refreshAcpRoots(ctx).then((n) => {
|
|
22454
|
+
console.log(`[serenity-hooks] 面 E:CCC 准入名单已刷新(${n} 个 CCC 启用 ACP 通道)`);
|
|
22455
|
+
});
|
|
22456
|
+
};
|
|
22457
|
+
try {
|
|
22458
|
+
ctx.on("settings/document-updated", (ns) => {
|
|
22459
|
+
if (String(ns) === "serenity-hooks") refresh();
|
|
22460
|
+
});
|
|
22461
|
+
} catch {}
|
|
22462
|
+
refresh();
|
|
21974
22463
|
sync();
|
|
21975
22464
|
}
|
|
21976
22465
|
//#endregion
|
|
@@ -555,4 +555,4 @@ function setWeixinEnabled(root, enabled) {
|
|
|
555
555
|
return next;
|
|
556
556
|
}
|
|
557
557
|
//#endregion
|
|
558
|
-
export { readWeixinCredential as a, weixinInboundDir as c, LOCALSTORE_SCOPES as d, checkLocalstoreGitCompliance as f,
|
|
558
|
+
export { runLocalStore as _, readWeixinCredential as a, weixinInboundDir as c, LOCALSTORE_SCOPES as d, checkLocalstoreGitCompliance as f, readStore as g, readGitTrack as h, matchWeixinRoute as i, weixinSessionIdFor as l, localstorePath as m, extractWeixinText as n, readWeixinSettings as o, getEntry as p, hasVoiceItem as r, sanitizeFileName as s, extractWeixinMedia as t, weixin_route_exports as u, writeEntry as v };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.59.0",
|
|
4
4
|
"description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/container_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 工作日志与原地重建 + 网页登录入口/微信桥/子角色/对外问答页/trajectory 唤醒注册表。适配 DSH 0.1.7-rc.1 与 0.2.0-rc.1(双档)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|