bothouse-dsh 0.1.55 → 0.1.57
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/collab/cloudweb.mjs +6 -4
- package/collab/devices.html +5 -1
- package/collab/dsh-member.mjs +2 -0
- package/collab/handoff-rules.json +17 -2
- package/collab/page.html +12 -2
- package/collab/pairing.mjs +12 -1
- package/collab/panel.mjs +20 -8
- package/collab/tasks.mjs +23 -0
- package/collab/team-exit-rules.json +1 -1
- package/collab/team-mode-rules.md +1 -1
- package/node_modules/bothouse-dsh-channels/client.js +303 -73
- package/package.json +1 -1
package/collab/cloudweb.mjs
CHANGED
|
@@ -54,7 +54,7 @@ const BARE_MENTION = '(只 @ 了你,没写内容:看上面的频道消息
|
|
|
54
54
|
export const PROGRESS_PREFIX = '__cc_connect_progress_card_v1__:';
|
|
55
55
|
// cancelled (#913): a card whose turn was cut without an outcome — a restarted bridge, a
|
|
56
56
|
// cancelled DSH job. page.html styles it like any other card state.
|
|
57
|
-
const CARD_STATES = { running: '处理中', completed: '已完成', failed: '失败', cancelled: '已取消' };
|
|
57
|
+
const CARD_STATES = { running: '处理中', completed: '已完成', failed: '失败', cancelled: '已取消', aborted: '已打断' };
|
|
58
58
|
const RESERVED_IDS = new Set(['user', 'system']);
|
|
59
59
|
const MAX_NAME = 64;
|
|
60
60
|
|
|
@@ -99,9 +99,9 @@ export function progressCardOf(p) {
|
|
|
99
99
|
const items = Array.isArray(p.items) ? p.items : (p.entries ?? []).filter(e => typeof e === 'string').map(text => ({ text }));
|
|
100
100
|
for (const it of items) {
|
|
101
101
|
const text = String(it?.text ?? '').replace(/\s+/g, ' ').trim();
|
|
102
|
-
if (!text) continue;
|
|
102
|
+
if (!text && it?.kind !== 'tool_use' && it?.kind !== 'tool_result') continue;
|
|
103
103
|
const tool = String(it.tool ?? '').trim();
|
|
104
|
-
lines.push(`${it.kind === 'tool_use' ? '🔧' : it.kind === 'error' ? '❌' : '•'} ${tool ? tool + ' — ' : ''}${text.slice(0, 120)}`);
|
|
104
|
+
lines.push(`${it.kind === 'tool_use' ? '🔧' : it.kind === 'error' ? '❌' : '•'} ${tool ? tool + (text ? ' — ' : '') : ''}${text.slice(0, 120)}`);
|
|
105
105
|
}
|
|
106
106
|
const agent = String(p.agent ?? 'Agent').trim() || 'Agent';
|
|
107
107
|
const run = toolSteps(items);
|
|
@@ -137,7 +137,9 @@ function toolSteps(items) {
|
|
|
137
137
|
if (it.kind === 'tool_use') steps.push({ tool, state: 0, input: clip(it.text), output: '' });
|
|
138
138
|
else if (it.kind === 'tool_result') {
|
|
139
139
|
const st = failed(it) ? 2 : 1;
|
|
140
|
-
|
|
140
|
+
// A repeated result updates its closed step, not another execution.
|
|
141
|
+
let i = open(tool);
|
|
142
|
+
if (i < 0) i = steps.findLastIndex(s => tool === '' || s.tool === tool);
|
|
141
143
|
if (i >= 0) Object.assign(steps[i], { state: st, output: clip(it.text) });
|
|
142
144
|
else steps.push({ tool, state: st, input: '', output: clip(it.text) });
|
|
143
145
|
} else if (it.kind === 'error') {
|
package/collab/devices.html
CHANGED
|
@@ -234,7 +234,11 @@ async function loadPeers() {
|
|
|
234
234
|
ul.append(li);
|
|
235
235
|
}
|
|
236
236
|
$('peersNote').textContent = '';
|
|
237
|
-
} catch (e) {
|
|
237
|
+
} catch (e) {
|
|
238
|
+
$('peersNote').textContent = '';
|
|
239
|
+
$('peers').replaceChildren(el('li', { class: 'empty' }, '没列出电脑,看下面的原因'));
|
|
240
|
+
$('ctlErr').textContent = e.message;
|
|
241
|
+
}
|
|
238
242
|
}
|
|
239
243
|
function select(p, li) {
|
|
240
244
|
selected = p;
|
package/collab/dsh-member.mjs
CHANGED
|
@@ -312,6 +312,8 @@ export class TurnProgress {
|
|
|
312
312
|
|
|
313
313
|
call(callId, name, args) {
|
|
314
314
|
if (this.ended) return;
|
|
315
|
+
// ACP updates for an existing call do not start another execution.
|
|
316
|
+
if (callId !== undefined && this.names.has(callId)) return;
|
|
315
317
|
this.steps++;
|
|
316
318
|
if (callId !== undefined) this.names.set(callId, name);
|
|
317
319
|
this.items.push({ kind: 'tool_use', tool: name, text: toolArgText(args) });
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
"标题行 = 去掉前导空白后以 # 开头的行;一节 = 从标题(或 inline 时以节名开头的行)到下一个标题。lead 是行首可去掉的字符。",
|
|
7
7
|
"正则只写 ASCII 字符类(JS 的 \\w、\\d 只认 ASCII,Python 认 Unicode),按 search 用,要锚定就写 ^。",
|
|
8
8
|
"checks 按顺序跑;ws-handoff 一次列出所有不合格项,DSH collab 报第一条。dispatch 列出只带字段的派活(/tasks:title、ref、base)要跑的检查:字段拼成一份交接再查。",
|
|
9
|
-
"改规则后跑 contrib/lead/test_handoff_rules.py(Python 与 Node 跑同一组 fixture 比对)。"
|
|
9
|
+
"改规则后跑 contrib/lead/test_handoff_rules.py(Python 与 Node 跑同一组 fixture 比对)。",
|
|
10
|
+
"tier:fact = 程序能拿外面的东西核对的检查(路径存在、原话搜得到、PR 查得到、不引用 /tmp),所有主角色都过;format = 只看有没有写、写多长,只在主角色后端是 DS 类(dsh、deepseek 模型)时开(ws-handoff 按 config.toml 里本项目的 agent 判)。带 verify 的 fact 检查再真去核对写的内容,一次最多核五条,问题一次报完。起因:用户 11-06 认可「核事实全开、核格式只给 DS」,强模型被格式检查打回六次没变聪明。DSH collab 只读本文件、跑全部检查(DSH 本身就是 DS 类)。"
|
|
10
11
|
],
|
|
11
12
|
"lead": "#-*> \t",
|
|
12
13
|
"checks": [
|
|
13
14
|
{
|
|
14
15
|
"id": "sections",
|
|
15
16
|
"type": "headings",
|
|
17
|
+
"tier": "format",
|
|
16
18
|
"names": [
|
|
17
19
|
"用户原话",
|
|
18
20
|
"总目标",
|
|
@@ -24,17 +26,27 @@
|
|
|
24
26
|
{
|
|
25
27
|
"id": "ref",
|
|
26
28
|
"type": "ref",
|
|
29
|
+
"tier": "fact",
|
|
27
30
|
"section": "参照实现",
|
|
28
31
|
"inline": true,
|
|
29
32
|
"none_re": "无\\s*[((]\\s*新建\\s*[))]",
|
|
30
33
|
"ref_re": "[~A-Za-z0-9_./-]*[A-Za-z_][~A-Za-z0-9_./-]*:[0-9]+",
|
|
31
34
|
"path_re": "[/.]",
|
|
32
35
|
"msg": "缺参照实现:写 文件:行,非代码活写「无(新建)」。示例:\n## 参照实现\n- bridge/contrib/lead/ws-up:120(和它的差异:…)",
|
|
33
|
-
"dispatch_msg": "缺参照实现:写 文件:行,非代码活写「无(新建)」。示例:ref: \"dsh/collab/tasks.mjs:226\",或 ref: \"无(新建)\""
|
|
36
|
+
"dispatch_msg": "缺参照实现:写 文件:行,非代码活写「无(新建)」。示例:ref: \"dsh/collab/tasks.mjs:226\",或 ref: \"无(新建)\"",
|
|
37
|
+
"verify": {
|
|
38
|
+
"kind": "paths",
|
|
39
|
+
"msg": "「参照实现」写的路径找不到:{items}。写真实存在的 文件:行(完整路径或 ~/ 开头),或改成「无(新建)」。",
|
|
40
|
+
"remote_re": "容器|服务器|远程|远端|隔壁|对面|另一台|别的机器|[Vv][Pp][Ss]|ssh|[Cc]ontainer|[Rr]emote|[Ss]erver",
|
|
41
|
+
"remote_sections": [
|
|
42
|
+
"总目标"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
34
45
|
},
|
|
35
46
|
{
|
|
36
47
|
"id": "scope",
|
|
37
48
|
"type": "kinds",
|
|
49
|
+
"tier": "format",
|
|
38
50
|
"section": "范围",
|
|
39
51
|
"line_re": "^(不?改)\\s*[::]\\s*\\S",
|
|
40
52
|
"need": [
|
|
@@ -47,6 +59,7 @@
|
|
|
47
59
|
{
|
|
48
60
|
"id": "base",
|
|
49
61
|
"type": "heading_needs_field",
|
|
62
|
+
"tier": "format",
|
|
50
63
|
"headings": [
|
|
51
64
|
"共改文件",
|
|
52
65
|
"多人改"
|
|
@@ -58,12 +71,14 @@
|
|
|
58
71
|
{
|
|
59
72
|
"id": "no_tmp",
|
|
60
73
|
"type": "forbid",
|
|
74
|
+
"tier": "fact",
|
|
61
75
|
"re": "(^|[\\s`'\"((「:=])/(private/)?tmp/",
|
|
62
76
|
"msg": "交接里引用了 /tmp 下的文件:下一棒(换了会话、重启过)常找不到。脚本和状态文件挪到角色目录(如 notes/、~/work/),交接里写完整路径。示例:\n- 上一棒的脚本:~/work/tools/render_cover.py(不写 /tmp/render.py)"
|
|
63
77
|
},
|
|
64
78
|
{
|
|
65
79
|
"id": "browser_tab",
|
|
66
80
|
"type": "needs_if",
|
|
81
|
+
"tier": "format",
|
|
67
82
|
"trigger_re": "(开|用|在|进|上)个?浏览器|浏览器(里|上|中|配置)|[Cc]hrome ?(里|上)|登录态下(操作|下单|点)",
|
|
68
83
|
"need_re": "(标签页|tab)\\s*(id|ID|Id)?\\s*[::=]\\s*([A-Za-z0-9_-]+|无)",
|
|
69
84
|
"msg": "浏览器活要写用哪个标签页:一行「标签页 id:<id>」,下一棒直接接着用,不用重新找页面、重新登录;还没开过页面写「标签页 id:无」。"
|
package/collab/page.html
CHANGED
|
@@ -1219,12 +1219,22 @@ function connect() {
|
|
|
1219
1219
|
$('input').addEventListener('keydown', (e) => {
|
|
1220
1220
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); $('form').requestSubmit(); }
|
|
1221
1221
|
});
|
|
1222
|
-
|
|
1222
|
+
// #205: fit the box to its text, or to the placeholder while empty — on a narrow window (phone,
|
|
1223
|
+
// Windows at 125%/150%) the hint wraps to two lines and a one-row box cut the second line off.
|
|
1224
|
+
// scrollHeight leaves the border out; add it back so border-box doesn't leave a sliver to scroll.
|
|
1225
|
+
function fitInput() {
|
|
1226
|
+
const t = $('input');
|
|
1227
|
+
t.style.height = 'auto';
|
|
1228
|
+
t.style.height = (t.scrollHeight + t.offsetHeight - t.clientHeight) + 'px';
|
|
1229
|
+
}
|
|
1230
|
+
$('input').addEventListener('input', fitInput);
|
|
1231
|
+
window.addEventListener('resize', fitInput, { passive: true });
|
|
1232
|
+
fitInput();
|
|
1223
1233
|
$('form').addEventListener('submit', async (e) => {
|
|
1224
1234
|
e.preventDefault();
|
|
1225
1235
|
const text = $('input').value.trim();
|
|
1226
1236
|
if (!text || state.room == null) return;
|
|
1227
|
-
$('input').value = '';
|
|
1237
|
+
$('input').value = ''; fitInput();
|
|
1228
1238
|
try {
|
|
1229
1239
|
const { message } = await api(`${ROOT}/collab/api/rooms/${state.room}/messages`, {
|
|
1230
1240
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
package/collab/pairing.mjs
CHANGED
|
@@ -250,9 +250,20 @@ export async function whoisName(ip, run = tailscale) {
|
|
|
250
250
|
} catch { return undefined; }
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Parse tailscale's --json output. The macOS app's CLI prints a plain sentence instead
|
|
255
|
+
* ("The Tailscale GUI failed to start: …") and still exits 0 when the app is not running,
|
|
256
|
+
* so a bare JSON.parse would put "Unexpected token 'T'…" on the devices page.
|
|
257
|
+
*/
|
|
258
|
+
export function tailscaleJSON(out) {
|
|
259
|
+
try { return JSON.parse(out); } catch {
|
|
260
|
+
throw new PairError('tailnet', 'Tailscale 没在运行或没登录,请先打开 Tailscale');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
253
264
|
/** Other machines in the tailnet: [{ name, ip, online, os }]. */
|
|
254
265
|
export async function tailnetPeers(run = tailscale) {
|
|
255
|
-
const status =
|
|
266
|
+
const status = tailscaleJSON(await run(['status', '--json']));
|
|
256
267
|
return Object.values(status?.Peer ?? {}).map(p => ({
|
|
257
268
|
name: String(p.HostName || p.DNSName || '').trim() || '未命名电脑',
|
|
258
269
|
ip: (p.TailscaleIPs ?? []).find(isTailnetAddress) ?? p.TailscaleIPs?.[0],
|
package/collab/panel.mjs
CHANGED
|
@@ -75,11 +75,21 @@ function sendJSON(res, status, body) {
|
|
|
75
75
|
res.end(JSON.stringify(body));
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const escapeHTML = v => String(v).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Page shown inside the frame when the panel cannot be reached: what is wrong, what to do,
|
|
82
|
+
* and a retry button, so a DSH-only user who switched the panel on is not left at a dead end.
|
|
83
|
+
*/
|
|
84
|
+
function sendNotice(res, status, reason, help) {
|
|
80
85
|
res.writeHead(status, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
81
|
-
res.end(`<!doctype html><meta charset="utf-8"><title>bothouse</title><
|
|
82
|
-
|
|
86
|
+
res.end(`<!doctype html><meta charset="utf-8"><title>bothouse</title><meta name="color-scheme" content="light dark">`
|
|
87
|
+
+ `<body style="font:14px/1.6 system-ui;padding:24px;max-width:560px;color:#666">`
|
|
88
|
+
+ `<h3 style="margin:0 0 8px;font-size:16px;color:inherit">打不开 bothouse 面板</h3>`
|
|
89
|
+
+ `<p style="margin:0 0 8px">${escapeHTML(reason)}</p>`
|
|
90
|
+
+ (help ? `<p style="margin:0 0 16px">${escapeHTML(help)}</p>` : '')
|
|
91
|
+
+ `<button type="button" onclick="location.reload()" style="font:inherit;padding:4px 14px;border-radius:6px;border:1px solid #bbb;background:transparent;color:inherit;cursor:pointer">重试</button>`
|
|
92
|
+
+ `</body>`);
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
/**
|
|
@@ -117,14 +127,16 @@ export function panelRoute(req, res, url, { bridgeConfig, fetchImpl = fetch }) {
|
|
|
117
127
|
return (async () => {
|
|
118
128
|
const rest = url.pathname.slice(PANEL_PREFIX.length) || '/';
|
|
119
129
|
const isAPI = rest.startsWith('/api/');
|
|
120
|
-
const fail = (status, reason) => (isAPI ? sendJSON(res, status, { ok: false, error: reason }) : sendNotice(res, status, reason), true);
|
|
130
|
+
const fail = (status, reason, help) => (isAPI ? sendJSON(res, status, { ok: false, error: reason }) : sendNotice(res, status, reason, help), true);
|
|
121
131
|
let settings;
|
|
122
132
|
try {
|
|
123
133
|
settings = managementSettings(readFileSync(bridgeConfig, 'utf8'));
|
|
124
134
|
} catch {
|
|
125
|
-
return fail(503,
|
|
135
|
+
return fail(503, `这台电脑没找到 bothouse:读不到 ${bridgeConfig}`,
|
|
136
|
+
'这个面板管理完整版 bothouse 的角色、用量和设置。装好 bothouse 并启动一次后点「重试」;只用频道的话,可以在「插件 → bothouse」里关掉「bothouse 面板」这一项。');
|
|
126
137
|
}
|
|
127
|
-
if (!settings.enabled) return fail(503,
|
|
138
|
+
if (!settings.enabled) return fail(503, `bothouse 的管理接口没打开:${bridgeConfig} 里 [management] 没有 enabled = true`,
|
|
139
|
+
'在那一节写上 enabled = true,重启 bothouse 后点「重试」。');
|
|
128
140
|
const upstream = `http://${dialHost(settings.host)}:${settings.port}${rest}${url.search}`;
|
|
129
141
|
const headers = {};
|
|
130
142
|
for (const name of PASS_REQUEST) {
|
|
@@ -143,7 +155,7 @@ export function panelRoute(req, res, url, { bridgeConfig, fetchImpl = fetch }) {
|
|
|
143
155
|
redirect: 'manual',
|
|
144
156
|
});
|
|
145
157
|
} catch {
|
|
146
|
-
return fail(502, '
|
|
158
|
+
return fail(502, `bothouse 没在运行:连不上 ${dialHost(settings.host)}:${settings.port}`, '启动 bothouse 后点「重试」。');
|
|
147
159
|
}
|
|
148
160
|
const out = {};
|
|
149
161
|
for (const name of PASS_RESPONSE) {
|
package/collab/tasks.mjs
CHANGED
|
@@ -41,6 +41,26 @@ export const CHECK_AFTER_S = 10 * 60;
|
|
|
41
41
|
export const MAX_RENEWALS = 0;
|
|
42
42
|
// setTimeout's own maximum delay (Node clamps anything larger to 1 ms).
|
|
43
43
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
44
|
+
/**
|
|
45
|
+
* The bridge watch posts its wake (「接着做 X,做完再结束本轮…」, i18n MsgWatchResume) and its notices
|
|
46
|
+
* (「看门:…」, MsgWatchIdleEscalate / MsgWatchFailEscalate …) in the lead's name. They chase work that
|
|
47
|
+
* already exists; read as a lead dispatch they opened a new task per wake (#221, 11-06: 「#6 接着做 -」).
|
|
48
|
+
* The text follows bridge/core/i18n.go; keep both in step.
|
|
49
|
+
*/
|
|
50
|
+
const WATCH_NOTICE_PREFIXES = ['看门:', '看門:', 'Watch: ', '見守り:'];
|
|
51
|
+
const WATCH_RESUME_LINES = [
|
|
52
|
+
/^接着做 .+,做完再结束本轮,交回时第一行写 \[report\]$/,
|
|
53
|
+
/^接著做 .+,做完再結束本輪,交回時第一行寫 \[report\]$/,
|
|
54
|
+
/^Continue .+ and finish it; when done, start the reply with \[report\]$/,
|
|
55
|
+
/^.+ を続けて完了させてください。完了したら返信の1行目を \[report\] にしてください$/,
|
|
56
|
+
/^Continúa .+ hasta terminarlo; al terminar, empieza la respuesta con \[report\]$/,
|
|
57
|
+
];
|
|
58
|
+
export function isWatchNotice(text, team = []) {
|
|
59
|
+
// A wake may have a handoff-file line after it. A report instruction elsewhere in a dispatch is not a wake.
|
|
60
|
+
const first = withoutLeadingMentions(String(text), team).split('\n', 1)[0].trim();
|
|
61
|
+
return WATCH_NOTICE_PREFIXES.some(p => first.startsWith(p)) || WATCH_RESUME_LINES.some(p => p.test(first));
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
const STANCE_LINE = /^\s*【(接|问|退|升)】\s*(?:#(\d+))?\s*(.*)$/;
|
|
45
65
|
// The dispatcher's explicit review of a workspace task, a line of its chat-channel reply:
|
|
46
66
|
// 【验收】#id closes it, 【返工】#id 原因 sends it back (Tasks.verdictsOf).
|
|
@@ -646,6 +666,9 @@ export class Tasks {
|
|
|
646
666
|
/** A member reply whose first line is 【接】【问】【退】【升】 (optionally #id) answers its task. */
|
|
647
667
|
fromMessage(msg) {
|
|
648
668
|
if (msg.author_kind !== 'role' || msg.card !== null) return undefined;
|
|
669
|
+
const lead = this.store.leadId(msg.room_id) === null ? this.hub.primaryMember() : this.hub.primaryIn(msg.room_id);
|
|
670
|
+
// Watch posts arrive as the lead. Ignore them before verdicts, stances, hand-backs or implicit reviews.
|
|
671
|
+
if (msg.author === lead?.id && isWatchNotice(msg.text, this.hub.routableIn(msg.room_id))) return undefined;
|
|
649
672
|
if (this.verdictsOf(msg)) return undefined;
|
|
650
673
|
this.settleCall(msg);
|
|
651
674
|
const stance = this.stanceOf(msg);
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"no_check_not_exempt": "「自验:无需」只限方向、花钱、需要用户看的成品、权限确认四类,其它类别要附证据"
|
|
31
31
|
},
|
|
32
32
|
"bounce": {
|
|
33
|
-
"text": "这条 [{kind}] 没有转给用户:{problems}。补上后重发:第一行写 [{kind}] #任务板编号 背景:……,再起一行写做了什么,再一行写「要你做的:……」,这三段系统原样贴到聊天频道;之后的行是细节,贴在那条消息下的子话题{extra}
|
|
33
|
+
"text": "这条 [{kind}] 没有转给用户:{problems}。补上后重发:第一行写 [{kind}] #任务板编号 背景:……,再起一行写做了什么,再一行写「要你做的:……」,这三段系统原样贴到聊天频道;之后的行是细节,贴在那条消息下的子话题{extra}。",
|
|
34
34
|
"sep": ";",
|
|
35
35
|
"extra": {
|
|
36
36
|
"ask": ",正文写「自验:」附证据和「类别:」;方向、花钱、需要用户看的成品、权限确认四类可写「自验:无需」"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
- 频道分两种:聊天频道(用户在这里聊天、被提问、看阶段结果)和它下面的工作区(干活的地方)。
|
|
3
3
|
- 多步骤的活先开公开任务卡(先发 --kind start);闲聊、问答不开。
|
|
4
4
|
- 聊天频道要保持畅通,大活交工作区:预计超过 10 分钟,或要改代码、跑测试、等命令的,谁接活谁这一轮写交接(副手被直接点名也一样)(至少写「用户原话」「总目标」「范围」三节和参照实现,范围每项写一行「改:…」;被退回按列出的一次补齐),跑 `ws-handoff <交接文件> "<任务>"`,不在聊天频道自己做;它不算开子 agent,不用另问用户;交出就结束本轮,不在本轮等或查进度,结果由工作区 [report] 转回。工作区里接到的活自己做。DSH 频道调 POST /collab/api/rooms/{聊天频道 id}/workspaces。一个工作区同一时间只做一件活,满 4 个就排队。
|
|
5
|
-
-
|
|
5
|
+
- 聊天频道里对真人说人话:讲场景、短段落,默认不用表格、不贴代码,细节进子话题。
|
|
6
6
|
- 同团队同平台成员在新频道默认可派;团队外成员才请用户发 `/bind <队友项目名>`,机器人自己发的 /bind 不生效。
|
|
7
7
|
- 在工作区里要问用户或交结果:最后一条回复第一行写 [ask] 或 [report] 加 #任务板编号和「背景:」,再一行做了什么,再一行「要你做的:」,再一行「我加的:」(用户原话里没有、你加的设定,没有写「无」);这四段贴到聊天频道,不写编号、代号、路径、表格;之后是细节,进子话题。在跑时有截图或阶段结果就 ws-up 推一条。
|
|
8
8
|
- 发 [ask] 后工作区让出、会话清空,用户回复后系统找空工作区带交接文件和答复接着做;交回最终 [report] 后系统自动标完成、清空会话、放回工作区。
|