jbc-dssi 1.0.1 → 1.1.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/index.js +23 -3
- package/package.json +5 -1
- package/vendor/bots/bridge/.gitkeep +0 -0
- package/vendor/bots/bridge/index.js +16 -0
- package/vendor/bots/bridge/meta.js +112 -0
- package/vendor/bots/bridge/view.js +207 -0
- package/vendor/bots/contract/.gitkeep +0 -0
- package/vendor/bots/contract/fixtures/team-list.json +165 -0
- package/vendor/bots/contract/teams.schema.json +78 -0
- package/vendor/dssAgent/bridge/distill.js +300 -0
- package/vendor/dssAgent/bridge/index.js +2719 -0
- package/vendor/groups/bridge/engine.js +569 -0
- package/vendor/groups/bridge/index.js +307 -0
- package/vendor/groups/bridge/mentions.js +66 -0
- package/vendor/groups/bridge/prompt.js +159 -0
- package/vendor/groups/bridge/state.js +65 -0
package/index.js
CHANGED
|
@@ -54,23 +54,43 @@ import { ensurePairing } from './pairing/ensure.js';
|
|
|
54
54
|
*
|
|
55
55
|
* 依赖方向:桥接 → bots/bridge(允许);bots/bridge 不反向依赖桥接。
|
|
56
56
|
*/
|
|
57
|
+
/**
|
|
58
|
+
* 加载可选子模块(bots / dssAgent / groups)。
|
|
59
|
+
*
|
|
60
|
+
* 两条路径都要能跑:
|
|
61
|
+
* · 发布态(npm 包装进 node_modules):模块被 prepack 复制到 ./vendor/<name>/
|
|
62
|
+
* · 开发态(在仓库里直接 link):模块是插件目录的同级目录 ../<name>/
|
|
63
|
+
* 两种找不到才算"模块缺失",此时能力自动关闭(不影响桥接主链路)。
|
|
64
|
+
*/
|
|
65
|
+
async function loadOptionalModule(name) {
|
|
66
|
+
let lastErr = null;
|
|
67
|
+
for (const spec of [`./vendor/${name}/bridge/index.js`, `../${name}/bridge/index.js`]) {
|
|
68
|
+
try {
|
|
69
|
+
return await import(spec);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
lastErr = err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
throw lastErr ?? new Error(`模块 ${name} 未找到`);
|
|
75
|
+
}
|
|
76
|
+
|
|
57
77
|
let botsModule = null;
|
|
58
78
|
try {
|
|
59
|
-
botsModule = await
|
|
79
|
+
botsModule = await loadOptionalModule('bots');
|
|
60
80
|
} catch (err) {
|
|
61
81
|
console.log(`[mob-bridge] bots 模块未加载(团队能力关闭):${err?.message ?? err}`);
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
let dssAgentModule = null;
|
|
65
85
|
try {
|
|
66
|
-
dssAgentModule = await
|
|
86
|
+
dssAgentModule = await loadOptionalModule('dssAgent');
|
|
67
87
|
} catch (err) {
|
|
68
88
|
console.log(`[mob-bridge] dssAgent 模块未加载(智能体能力关闭):${err?.message ?? err}`);
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
let groupsModule = null;
|
|
72
92
|
try {
|
|
73
|
-
groupsModule = await
|
|
93
|
+
groupsModule = await loadOptionalModule('groups');
|
|
74
94
|
} catch (err) {
|
|
75
95
|
console.log(`[mob-bridge] groups 模块未加载(群聊能力关闭):${err?.message ?? err}`);
|
|
76
96
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jbc-dssi",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "鲸编程 - 不只是手机遥控操作电脑,而是将电脑级的Agent能力装进手机",
|
|
6
6
|
"keywords": [
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"index.js",
|
|
29
29
|
"lib/",
|
|
30
30
|
"pairing/",
|
|
31
|
+
"vendor/",
|
|
31
32
|
"cordis.patch.yml"
|
|
32
33
|
],
|
|
33
34
|
"exports": {
|
|
@@ -41,5 +42,8 @@
|
|
|
41
42
|
"publishConfig": {
|
|
42
43
|
"access": "public",
|
|
43
44
|
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"prepack": "node scripts/build-vendor.mjs"
|
|
44
48
|
}
|
|
45
49
|
}
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bots/bridge/index.js —— 桥接侧唯一公开入口(被 jbc-dssi 动态加载)。
|
|
3
|
+
*
|
|
4
|
+
* 契约:纯函数 + 一个本地小文件存储工厂;不依赖 DSH、不引用桥接内部实现。
|
|
5
|
+
* 状态(seq / 去重签名 / 防抖定时器)属于桥接连接自己 —— 这样本模块可单测、
|
|
6
|
+
* 可删除(目录没了 → 动态 import 失败 → 桥接自动关闭团队能力,照常工作)。
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
PAYLOAD_VERSION,
|
|
10
|
+
LIMITS,
|
|
11
|
+
isTeamEvent,
|
|
12
|
+
teamFromItem,
|
|
13
|
+
buildSnapshot,
|
|
14
|
+
snapshotSignature,
|
|
15
|
+
} from './view.js';
|
|
16
|
+
export { createTeamMetaStore } from './meta.js';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bots/bridge/meta.js —— 团队显示信息(团队名 + 成员中文显示名)的持久化。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:DSH 的 Team 投影里只有成员内部名(小写英文、永久不可改)
|
|
5
|
+
* 和会话标题,没有"团队名"和"中文显示名"这两个产品字段。
|
|
6
|
+
* 这张别名表存在**电脑侧**(不是手机本地),这样手机和浏览器看到同一份。
|
|
7
|
+
*
|
|
8
|
+
* 数据极小(团队数 × 成员数),读写都在事件循环外的一次小文件里;
|
|
9
|
+
* 写入用 tmp+rename,避免半截 JSON。
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_META_FILE = path.join(os.homedir(), '.dsh', 'jbc-dssi', 'bots-teams.json');
|
|
16
|
+
|
|
17
|
+
const LIMITS = {
|
|
18
|
+
teams: 100,
|
|
19
|
+
nameChars: 80,
|
|
20
|
+
aliases: 16,
|
|
21
|
+
idChars: 32,
|
|
22
|
+
labelChars: 40,
|
|
23
|
+
};
|
|
24
|
+
const ID_RE = /^[a-z0-9_-]{1,32}$/;
|
|
25
|
+
|
|
26
|
+
function cleanName(value) {
|
|
27
|
+
if (typeof value !== 'string') return '';
|
|
28
|
+
return value.trim().slice(0, LIMITS.nameChars);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanAliases(value) {
|
|
32
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
33
|
+
const out = {};
|
|
34
|
+
let n = 0;
|
|
35
|
+
for (const [id, label] of Object.entries(value)) {
|
|
36
|
+
if (n >= LIMITS.aliases) break;
|
|
37
|
+
const key = String(id).trim().toLowerCase();
|
|
38
|
+
const text = cleanName(label).slice(0, LIMITS.labelChars);
|
|
39
|
+
if (!ID_RE.test(key) || !text) continue;
|
|
40
|
+
out[key] = text;
|
|
41
|
+
n += 1;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @returns {{get:(sessionId:string)=>{name:string,aliases:Object}|undefined, all:()=>Object, set:Function, file:string}} */
|
|
47
|
+
export function createTeamMetaStore({ file, log = () => {} } = {}) {
|
|
48
|
+
const target = typeof file === 'string' && file.trim() ? file.trim() : DEFAULT_META_FILE;
|
|
49
|
+
/** @type {Record<string,{name:string,aliases:Record<string,string>}>} */
|
|
50
|
+
let cache = {};
|
|
51
|
+
let loaded = false;
|
|
52
|
+
|
|
53
|
+
function load() {
|
|
54
|
+
if (loaded) return;
|
|
55
|
+
loaded = true;
|
|
56
|
+
try {
|
|
57
|
+
const raw = fs.readFileSync(target, 'utf8');
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return;
|
|
60
|
+
for (const [sessionId, value] of Object.entries(parsed)) {
|
|
61
|
+
if (typeof sessionId !== 'string' || !sessionId) continue;
|
|
62
|
+
if (!value || typeof value !== 'object') continue;
|
|
63
|
+
const name = cleanName(value.name);
|
|
64
|
+
const aliases = cleanAliases(value.aliases);
|
|
65
|
+
if (!name && Object.keys(aliases).length === 0) continue;
|
|
66
|
+
cache[sessionId] = { name, aliases };
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// 文件不存在是常态(第一次用);损坏则忽略并记录,绝不影响团队功能
|
|
70
|
+
if (err?.code !== 'ENOENT') log(`[bots-meta] 读取失败,按空表处理:${err?.message ?? err}`);
|
|
71
|
+
cache = {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function persist() {
|
|
76
|
+
try {
|
|
77
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
78
|
+
const tmp = `${target}.tmp`;
|
|
79
|
+
fs.writeFileSync(tmp, JSON.stringify(cache, null, 2), 'utf8');
|
|
80
|
+
fs.renameSync(tmp, target);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
log(`[bots-meta] 写入失败:${err?.message ?? err}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
file: target,
|
|
88
|
+
get(sessionId) {
|
|
89
|
+
load();
|
|
90
|
+
return cache[sessionId];
|
|
91
|
+
},
|
|
92
|
+
all() {
|
|
93
|
+
load();
|
|
94
|
+
return cache;
|
|
95
|
+
},
|
|
96
|
+
set(sessionId, value) {
|
|
97
|
+
load();
|
|
98
|
+
const id = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
99
|
+
if (!id) return false;
|
|
100
|
+
const name = cleanName(value?.name);
|
|
101
|
+
const aliases = cleanAliases(value?.aliases);
|
|
102
|
+
if (!name && Object.keys(aliases).length === 0) return false;
|
|
103
|
+
// 团队数上限:满了就只允许更新已有团队(绝不因为一条脏消息丢掉整张表)
|
|
104
|
+
if (!cache[id] && Object.keys(cache).length >= LIMITS.teams) return false;
|
|
105
|
+
cache[id] = { name, aliases };
|
|
106
|
+
persist();
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { LIMITS as TEAM_META_LIMITS };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bots/bridge/view.js —— 纯函数层:把 DSH 的会话投影变成手机要的团队快照。
|
|
3
|
+
*
|
|
4
|
+
* 为什么这一层必须是纯函数:
|
|
5
|
+
* 1. 可单测:不需要起 DSH、不需要真团队,fixture 就能覆盖全部边界;
|
|
6
|
+
* 2. 上游(agent-team 是实验性包)改字段时,**只改这一层**,桥接与手机不动;
|
|
7
|
+
* 3. 大小可控:截断/上限集中在一处,避免某天一个 5000 字任务描述把帧撑爆。
|
|
8
|
+
*
|
|
9
|
+
* 数据来源(DSH v1):
|
|
10
|
+
* sessionController.list() 的每一项 item:
|
|
11
|
+
* { sessionId, running, cwd, updatedAt, origin?, parentSessionId?,
|
|
12
|
+
* projections: { values: { title?, agentTeam?: { members, tasks, failure? } } } }
|
|
13
|
+
* 成员的 running 状态不在 agentTeam 里 —— 要拿成员的会话 id 去 item 列表里 join。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 载荷版本。字段语义有破坏性变化时 +1,手机侧按 v 决定能不能解析。 */
|
|
17
|
+
export const PAYLOAD_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 上限。与 bots/contract/teams.schema.json 的 limits **必须一致**(测试会断言)。
|
|
21
|
+
* 取值依据:DSH agent-team 的硬上限是 16 成员 / 256 任务;这里再留一点余量,
|
|
22
|
+
* 保证最坏情况单帧远小于 MAX_FRAME_BYTES(8 MB),4G 上也不会卡首屏。
|
|
23
|
+
*/
|
|
24
|
+
export const LIMITS = Object.freeze({
|
|
25
|
+
teams: 20,
|
|
26
|
+
membersPerTeam: 16,
|
|
27
|
+
tasksPerTeam: 200,
|
|
28
|
+
subjectChars: 120,
|
|
29
|
+
descriptionChars: 300,
|
|
30
|
+
failureChars: 500,
|
|
31
|
+
errorChars: 300,
|
|
32
|
+
writeScopes: 20,
|
|
33
|
+
writeScopeChars: 100,
|
|
34
|
+
blockedBy: 20,
|
|
35
|
+
warnings: 20,
|
|
36
|
+
warningChars: 120,
|
|
37
|
+
nameChars: 80,
|
|
38
|
+
cwdChars: 300,
|
|
39
|
+
labelChars: 40,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** 团队事件:roster/任务板变了。历史 team/message/* 事件刻意不算。 */
|
|
43
|
+
const TEAM_EVENT_RE = /^team\/(?:member|task)$/;
|
|
44
|
+
|
|
45
|
+
export function isTeamEvent(type) {
|
|
46
|
+
return typeof type === 'string' && TEAM_EVENT_RE.test(type);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function str(value, max) {
|
|
50
|
+
if (typeof value !== 'string') return '';
|
|
51
|
+
const text = value.trim();
|
|
52
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function intOr(value, fallback = 0) {
|
|
56
|
+
return Number.isSafeInteger(value) ? value : fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function basename(p) {
|
|
60
|
+
if (typeof p !== 'string') return '';
|
|
61
|
+
const trimmed = p.replace(/\/+$/, '');
|
|
62
|
+
const tail = trimmed.slice(trimmed.lastIndexOf('/') + 1);
|
|
63
|
+
return tail.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function teamName({ title, cwd }) {
|
|
67
|
+
const fromTitle = str(title, LIMITS.nameChars);
|
|
68
|
+
if (fromTitle) return fromTitle;
|
|
69
|
+
const fromCwd = basename(cwd).slice(0, LIMITS.nameChars);
|
|
70
|
+
return fromCwd || '未命名小队';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 成员 id → 该会话是否在跑。注意:子会话也在 list() 里,不能被过滤掉。 */
|
|
74
|
+
function runningBySessionId(items) {
|
|
75
|
+
const map = new Map();
|
|
76
|
+
for (const item of items) {
|
|
77
|
+
if (item && typeof item.sessionId === 'string') map.set(item.sessionId, item.running === true);
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeMember(member, runningById, aliases = {}) {
|
|
83
|
+
if (!member || typeof member !== 'object') return null;
|
|
84
|
+
const name = str(member.name, LIMITS.nameChars);
|
|
85
|
+
if (!name) return null;
|
|
86
|
+
const role = member.role === 'lead' ? 'lead' : 'teammate';
|
|
87
|
+
const phase = ['failed', 'provisioning', 'active'].includes(member.phase) ? member.phase : 'active';
|
|
88
|
+
const out = {
|
|
89
|
+
name,
|
|
90
|
+
sessionId: str(member.id, 64),
|
|
91
|
+
role,
|
|
92
|
+
phase,
|
|
93
|
+
running: runningById.get(member.id) === true,
|
|
94
|
+
};
|
|
95
|
+
// 中文显示名来自电脑侧别名表(meta.js);没有就回退到内部名,UI 自己兜底
|
|
96
|
+
if (aliases && typeof aliases === 'object') {
|
|
97
|
+
const label = str(aliases[name], LIMITS.labelChars);
|
|
98
|
+
if (label) out.displayName = label;
|
|
99
|
+
}
|
|
100
|
+
const error = str(member.error, LIMITS.errorChars);
|
|
101
|
+
if (error) out.error = error;
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeTask(task, aliases = {}) {
|
|
106
|
+
if (!task || typeof task !== 'object') return null;
|
|
107
|
+
const subject = str(task.subject, LIMITS.subjectChars);
|
|
108
|
+
if (!subject) return null;
|
|
109
|
+
const status = ['completed', 'pending', 'in_progress', 'deleted'].includes(task.status)
|
|
110
|
+
? task.status
|
|
111
|
+
: 'pending';
|
|
112
|
+
if (status === 'deleted') return null;
|
|
113
|
+
const out = {
|
|
114
|
+
id: str(task.id, 32),
|
|
115
|
+
revision: intOr(task.revision, 0),
|
|
116
|
+
subject,
|
|
117
|
+
status,
|
|
118
|
+
ready: task.ready === true,
|
|
119
|
+
blockedBy: Array.isArray(task.blockedBy)
|
|
120
|
+
? task.blockedBy.slice(0, LIMITS.blockedBy).map((id) => str(id, 32))
|
|
121
|
+
: [],
|
|
122
|
+
writeScopes: Array.isArray(task.writeScopes)
|
|
123
|
+
? task.writeScopes.slice(0, LIMITS.writeScopes).map((p) => str(p, LIMITS.writeScopeChars))
|
|
124
|
+
: [],
|
|
125
|
+
writeScopeWarnings: Array.isArray(task.writeScopeWarnings)
|
|
126
|
+
? task.writeScopeWarnings.slice(0, LIMITS.warnings).map((w) => str(w, LIMITS.warningChars))
|
|
127
|
+
: [],
|
|
128
|
+
};
|
|
129
|
+
const description = str(task.description, LIMITS.descriptionChars);
|
|
130
|
+
if (description) out.description = description;
|
|
131
|
+
const owner = str(task.ownerName, LIMITS.nameChars);
|
|
132
|
+
if (owner) out.ownerName = str(aliases[owner], LIMITS.labelChars) || owner;
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 一个会话 item → 一支小队;不是小队就返回 null。
|
|
138
|
+
*
|
|
139
|
+
* "是不是小队"的判据:至少 1 个 teammate,或任务板非空。
|
|
140
|
+
* 只有 Lead 自己、也没有任务的全话不显示 —— 否则团队 tab 会把所有聊天会话都列出来。
|
|
141
|
+
*/
|
|
142
|
+
export function teamFromItem(item, runningById = new Map(), meta = null) {
|
|
143
|
+
const at = item?.projections?.values?.agentTeam;
|
|
144
|
+
if (!at || typeof at !== 'object' || !Array.isArray(at.members)) return null;
|
|
145
|
+
|
|
146
|
+
const labels = meta && typeof meta === 'object' && meta.aliases ? meta.aliases : {};
|
|
147
|
+
const members = at.members
|
|
148
|
+
.map((m) => normalizeMember(m, runningById, labels))
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.slice(0, LIMITS.membersPerTeam);
|
|
151
|
+
const tasks = (Array.isArray(at.tasks) ? at.tasks : [])
|
|
152
|
+
.map((task) => normalizeTask(task, labels))
|
|
153
|
+
.filter(Boolean)
|
|
154
|
+
.slice(0, LIMITS.tasksPerTeam);
|
|
155
|
+
|
|
156
|
+
const hasTeammate = members.some((m) => m.role === 'teammate');
|
|
157
|
+
if (!hasTeammate && tasks.length === 0) return null;
|
|
158
|
+
|
|
159
|
+
const team = {
|
|
160
|
+
sessionId: str(item.sessionId, 64),
|
|
161
|
+
name: (meta && str(meta.name, LIMITS.nameChars)) || teamName({
|
|
162
|
+
title: item?.projections?.values?.title,
|
|
163
|
+
cwd: item.cwd,
|
|
164
|
+
}),
|
|
165
|
+
cwd: str(item.cwd, LIMITS.cwdChars),
|
|
166
|
+
running: item.running === true,
|
|
167
|
+
updatedAt: intOr(item.updatedAt, 0),
|
|
168
|
+
members,
|
|
169
|
+
tasks,
|
|
170
|
+
};
|
|
171
|
+
const failure = str(at.failure, LIMITS.failureChars);
|
|
172
|
+
if (failure) team.failure = failure;
|
|
173
|
+
return team;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 全量快照。teams 按最近活动倒序,稳定排序(同时间按 sessionId)。 */
|
|
177
|
+
export function buildSnapshot(items, { seq, now = Date.now(), meta = null } = {}) {
|
|
178
|
+
const list = Array.isArray(items) ? items : [];
|
|
179
|
+
const runningById = runningBySessionId(list);
|
|
180
|
+
const metaBySession = meta && typeof meta === 'object' ? meta : {};
|
|
181
|
+
const teams = [];
|
|
182
|
+
for (const item of list) {
|
|
183
|
+
// 小队只能长在普通会话上:子智能体会话不能当 Lead,也不该出现在团队列表
|
|
184
|
+
if (!item || item.origin === 'subagent') continue;
|
|
185
|
+
try {
|
|
186
|
+
const team = teamFromItem(item, runningById, metaBySession[item.sessionId] ?? null);
|
|
187
|
+
if (team) teams.push(team);
|
|
188
|
+
} catch {
|
|
189
|
+
// 单条坏数据只跳过这一条,绝不让整份快照失败(失败必须可见,但不能连累别人)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
teams.sort((a, b) => b.updatedAt - a.updatedAt || a.sessionId.localeCompare(b.sessionId));
|
|
193
|
+
return {
|
|
194
|
+
v: PAYLOAD_VERSION,
|
|
195
|
+
seq: intOr(seq, 0),
|
|
196
|
+
asOf: intOr(now, 0),
|
|
197
|
+
teams: teams.slice(0, LIMITS.teams),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 快照指纹:只对 teams 做签名(忽略 seq/asOf)。
|
|
203
|
+
* 用途:实时事件触发的重发去重 —— 团队没变就不发,省流量也省手机重绘。
|
|
204
|
+
*/
|
|
205
|
+
export function snapshotSignature(snapshot) {
|
|
206
|
+
return JSON.stringify(snapshot?.teams ?? []);
|
|
207
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_note": "合成样本:字段形状抄自 DSH SessionProjectionMap.agentTeam(v1)+ sessionController.list() 的 Session 摘要。S2 联调后把真实抓到的样本替换/补充到这里(含真实 team/member、team/task 事件)。",
|
|
3
|
+
"items": [
|
|
4
|
+
{
|
|
5
|
+
"sessionId": "session-lead-a",
|
|
6
|
+
"running": false,
|
|
7
|
+
"agentAvailable": true,
|
|
8
|
+
"blank": false,
|
|
9
|
+
"cwd": "/tmp/example-project",
|
|
10
|
+
"updatedAt": 1791700000000,
|
|
11
|
+
"origin": null,
|
|
12
|
+
"parentSessionId": null,
|
|
13
|
+
"projections": {
|
|
14
|
+
"kind": "sequenced",
|
|
15
|
+
"asOfSeq": 120,
|
|
16
|
+
"values": {
|
|
17
|
+
"title": "给鲸编程加团队功能",
|
|
18
|
+
"agentTeam": {
|
|
19
|
+
"members": [
|
|
20
|
+
{ "id": "session-lead-a", "name": "lead", "role": "lead", "phase": "active" },
|
|
21
|
+
{ "id": "session-member-a1", "name": "reviewer", "role": "teammate", "phase": "active" },
|
|
22
|
+
{ "id": "session-member-a2", "name": "tester", "role": "teammate", "phase": "provisioning" }
|
|
23
|
+
],
|
|
24
|
+
"tasks": [
|
|
25
|
+
{
|
|
26
|
+
"id": "task-1",
|
|
27
|
+
"revision": 3,
|
|
28
|
+
"subject": "审阅 bots/bridge/view.js 的截断逻辑",
|
|
29
|
+
"description": "重点看成员 running join 与任务截断;写一份不超过 10 条的结论。",
|
|
30
|
+
"status": "in_progress",
|
|
31
|
+
"blockedBy": [],
|
|
32
|
+
"writeScopes": ["bots/bridge/"],
|
|
33
|
+
"ownerName": "reviewer",
|
|
34
|
+
"ready": true,
|
|
35
|
+
"writeScopeWarnings": []
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "task-2",
|
|
39
|
+
"revision": 1,
|
|
40
|
+
"subject": "补 payload 契约测试",
|
|
41
|
+
"description": "覆盖 v/seq/teams 形状与上限。",
|
|
42
|
+
"status": "pending",
|
|
43
|
+
"blockedBy": ["task-1"],
|
|
44
|
+
"writeScopes": ["bots/test/"],
|
|
45
|
+
"ownerName": "tester",
|
|
46
|
+
"ready": false,
|
|
47
|
+
"writeScopeWarnings": []
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "task-3",
|
|
51
|
+
"revision": 4,
|
|
52
|
+
"subject": "跑基线 115 条回归",
|
|
53
|
+
"description": "node test/run.mjs 必须全绿。",
|
|
54
|
+
"status": "completed",
|
|
55
|
+
"blockedBy": [],
|
|
56
|
+
"writeScopes": [],
|
|
57
|
+
"ownerName": "lead",
|
|
58
|
+
"ready": true,
|
|
59
|
+
"writeScopeWarnings": []
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "task-4",
|
|
63
|
+
"revision": 2,
|
|
64
|
+
"subject": "已删除的任务不应下发",
|
|
65
|
+
"status": "deleted",
|
|
66
|
+
"blockedBy": [],
|
|
67
|
+
"writeScopes": [],
|
|
68
|
+
"ready": false,
|
|
69
|
+
"writeScopeWarnings": []
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"sessionId": "session-member-a1",
|
|
78
|
+
"running": true,
|
|
79
|
+
"agentAvailable": true,
|
|
80
|
+
"blank": false,
|
|
81
|
+
"cwd": "/tmp/example-project",
|
|
82
|
+
"updatedAt": 1791700001000,
|
|
83
|
+
"origin": "subagent",
|
|
84
|
+
"parentSessionId": "session-lead-a",
|
|
85
|
+
"projections": { "values": {} }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"sessionId": "session-member-a2",
|
|
89
|
+
"running": false,
|
|
90
|
+
"agentAvailable": false,
|
|
91
|
+
"blank": false,
|
|
92
|
+
"cwd": "/tmp/example-project",
|
|
93
|
+
"updatedAt": 1791700001500,
|
|
94
|
+
"origin": "subagent",
|
|
95
|
+
"parentSessionId": "session-lead-a",
|
|
96
|
+
"projections": { "values": {} }
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"sessionId": "session-lead-b",
|
|
100
|
+
"running": true,
|
|
101
|
+
"agentAvailable": true,
|
|
102
|
+
"blank": false,
|
|
103
|
+
"cwd": "/Users/example/project",
|
|
104
|
+
"updatedAt": 1791700002000,
|
|
105
|
+
"origin": null,
|
|
106
|
+
"parentSessionId": null,
|
|
107
|
+
"projections": {
|
|
108
|
+
"kind": "sequenced",
|
|
109
|
+
"asOfSeq": 40,
|
|
110
|
+
"values": {
|
|
111
|
+
"title": "示例项目 · 检查更新浮层",
|
|
112
|
+
"agentTeam": {
|
|
113
|
+
"members": [
|
|
114
|
+
{ "id": "session-lead-b", "name": "lead", "role": "lead", "phase": "active" },
|
|
115
|
+
{ "id": "session-member-b1", "name": "coder", "role": "teammate", "phase": "failed", "error": "spawn 超时:provider 未在 30s 内返回 child" }
|
|
116
|
+
],
|
|
117
|
+
"tasks": [],
|
|
118
|
+
"failure": "持久 Team 记录被拒绝:TEAM_TASK_STALE_REVISION"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"sessionId": "session-member-b1",
|
|
125
|
+
"running": false,
|
|
126
|
+
"agentAvailable": false,
|
|
127
|
+
"blank": false,
|
|
128
|
+
"cwd": "/Users/example/project",
|
|
129
|
+
"updatedAt": 1791700003000,
|
|
130
|
+
"origin": "subagent",
|
|
131
|
+
"parentSessionId": "session-lead-b",
|
|
132
|
+
"projections": { "values": {} }
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"sessionId": "session-plain",
|
|
136
|
+
"running": false,
|
|
137
|
+
"agentAvailable": true,
|
|
138
|
+
"blank": false,
|
|
139
|
+
"cwd": "/tmp/example-project",
|
|
140
|
+
"updatedAt": 1791700004000,
|
|
141
|
+
"origin": null,
|
|
142
|
+
"parentSessionId": null,
|
|
143
|
+
"projections": { "values": { "title": "普通聊天:没有小队" } }
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"sessionId": "session-lead-c",
|
|
147
|
+
"running": false,
|
|
148
|
+
"agentAvailable": true,
|
|
149
|
+
"blank": false,
|
|
150
|
+
"cwd": "/tmp",
|
|
151
|
+
"updatedAt": 1791700005000,
|
|
152
|
+
"origin": null,
|
|
153
|
+
"parentSessionId": null,
|
|
154
|
+
"projections": {
|
|
155
|
+
"values": {
|
|
156
|
+
"title": "只有 Lead、没有任务",
|
|
157
|
+
"agentTeam": {
|
|
158
|
+
"members": [{ "id": "session-lead-c", "name": "lead", "role": "lead", "phase": "active" }],
|
|
159
|
+
"tasks": []
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bots-teams",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"description": "BOTS 帧(0x2B)载荷契约。字段语义与上限的事实来源;与 bots/bridge/view.js 的 LIMITS 由测试断言一致。",
|
|
5
|
+
"frames": {
|
|
6
|
+
"BOTS": "0x2B",
|
|
7
|
+
"BOTS_REFRESH": "0x2C",
|
|
8
|
+
"BOTS_META": "0x2D"
|
|
9
|
+
},
|
|
10
|
+
"limits": {
|
|
11
|
+
"teams": 20,
|
|
12
|
+
"membersPerTeam": 16,
|
|
13
|
+
"tasksPerTeam": 200,
|
|
14
|
+
"subjectChars": 120,
|
|
15
|
+
"descriptionChars": 300,
|
|
16
|
+
"failureChars": 500,
|
|
17
|
+
"errorChars": 300,
|
|
18
|
+
"writeScopes": 20,
|
|
19
|
+
"writeScopeChars": 100,
|
|
20
|
+
"blockedBy": 20,
|
|
21
|
+
"warnings": 20,
|
|
22
|
+
"warningChars": 120,
|
|
23
|
+
"nameChars": 80,
|
|
24
|
+
"cwdChars": 300,
|
|
25
|
+
"labelChars": 40
|
|
26
|
+
},
|
|
27
|
+
"payload": {
|
|
28
|
+
"v": "integer,载荷版本(当前 1)",
|
|
29
|
+
"seq": "integer,每条连接内单调递增;手机按 seq 丢弃过期快照",
|
|
30
|
+
"asOf": "epoch 毫秒,快照生成时刻",
|
|
31
|
+
"teams": [
|
|
32
|
+
{
|
|
33
|
+
"sessionId": "string,Lead 会话 id(TeamId === SessionId)",
|
|
34
|
+
"name": "string,Lead 会话标题,缺失退 cwd 末段,再退「未命名小队」",
|
|
35
|
+
"cwd": "string,共享工作目录",
|
|
36
|
+
"running": "boolean,Lead 会话是否有轮次在跑",
|
|
37
|
+
"updatedAt": "integer,epoch 毫秒",
|
|
38
|
+
"members": [
|
|
39
|
+
{
|
|
40
|
+
"name": "string,团队内唯一名字(DSH 保证不复用)",
|
|
41
|
+
"role": "lead | teammate",
|
|
42
|
+
"phase": "active | provisioning | failed",
|
|
43
|
+
"running": "boolean,该成员自己的会话当前是否有轮次在跑",
|
|
44
|
+
"error": "string?,phase=failed 时的原因",
|
|
45
|
+
"sessionId": "string?,成员会话 id(用于和子 Agent 会话对齐)",
|
|
46
|
+
"displayName": "string?,中文显示名(电脑侧别名表,BOTS_META 上传);缺失时 UI 用 name"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"tasks": [
|
|
50
|
+
{
|
|
51
|
+
"id": "string,形如 task-1",
|
|
52
|
+
"revision": "integer,CAS 版本;手机按 revision 丢弃旧编辑结果",
|
|
53
|
+
"subject": "string",
|
|
54
|
+
"description": "string?",
|
|
55
|
+
"status": "pending | in_progress | completed(deleted 不下发)",
|
|
56
|
+
"ownerName": "string?,成员 target 名(与 members[].name 对齐)",
|
|
57
|
+
"ready": "boolean,依赖是否全部完成(可 claim)",
|
|
58
|
+
"blockedBy": [
|
|
59
|
+
"string,task id"
|
|
60
|
+
],
|
|
61
|
+
"writeScopes": [
|
|
62
|
+
"string,workspace 相对路径前缀(仅提示,不强制)"
|
|
63
|
+
],
|
|
64
|
+
"writeScopeWarnings": [
|
|
65
|
+
"string,与进行中任务写作用域重叠的警告"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"failure": "string?,投影报告的最后一条持久 Team 记录失败原因"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"compatibility": {
|
|
74
|
+
"oldApp": "不认识 0x2B 的客户端会忽略;插件只在 bots 模块启用时发送",
|
|
75
|
+
"oldBridge": "HELLO 不带 caps:[bots];新 App 显示「电脑侧插件需更新」",
|
|
76
|
+
"unknownFields": "两端解析都必须忽略未知字段(只做加法)"
|
|
77
|
+
}
|
|
78
|
+
}
|