dsh-foreman 0.1.1 → 0.3.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/CHANGELOG.md +2 -89
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/cordis.patch.yml +7 -7
- package/icon.svg +0 -0
- package/lib/client.js +153 -2
- package/lib/index.js +33 -2
- package/locale/en.json +0 -0
- package/locale/zh.json +0 -0
- package/package.json +2 -2
- package/prompts/admission.md +0 -1
- package/prompts/cli.md +0 -0
- package/prompts/dispatch.md +0 -0
- package/prompts/handoff-orchestrator.md +0 -0
- package/prompts/handoff-worker.md +0 -0
- package/prompts/handoff.md +0 -0
- package/prompts/orchestrator.md +0 -0
- package/prompts/replies.md +0 -0
- package/prompts/shared.md +0 -0
- package/prompts/subscriptions.md +0 -0
- package/prompts/team/communication.md +0 -0
- package/prompts/team/delivery.md +0 -0
- package/prompts/team/scope.md +0 -0
- package/prompts/work.md +0 -0
- package/prompts/worker.md +0 -0
- package/scripts/add-device.mjs +0 -0
- package/scripts/dsh-foreman +0 -0
- package/scripts/migrate.mjs +0 -0
- package/skills/dsh-foreman-feishu/SKILL.md +0 -0
- package/skills/dsh-foreman-github/SKILL.md +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,95 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
下面 `0.2.0` 一节是从代码基线 `dsh-superagent-local` 沿袭下来的、那一份代码相对上游
|
|
5
|
-
`dsh-superagent-prototype` 的改动记录,保留它是为了说明基线里已经有什么;**它不是本项目的发布版本**。
|
|
3
|
+
本仓库是 dsh-foreman(研究方向原型)的本地改造版。下面是相对上游 0.1.0 的改动。
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## 0.1.1 — 2026-11-07
|
|
10
|
-
|
|
11
|
-
修的是 `0.1.0` 发布时才暴露的两个问题,都是真机验证(上海服务器 srv-02)发现的。
|
|
12
|
-
|
|
13
|
-
### 修复:运维脚本没有进包
|
|
14
|
-
|
|
15
|
-
`0.1.0` 的 `files` 漏了 `scripts/`,于是装完 `dsh-foreman@0.1.0` 之后
|
|
16
|
-
`node scripts/add-device.mjs` **不存在** —— 而那是加设备的唯一入口,
|
|
17
|
-
文档里却写着让人这么跑。现在随包发三个脚本:
|
|
18
|
-
|
|
19
|
-
- `scripts/add-device.mjs`(加设备 / 换令牌 / 吊销)
|
|
20
|
-
- `scripts/migrate.mjs`(迁机打包与自检)
|
|
21
|
-
- `scripts/dsh-foreman`(服务脚本)
|
|
22
|
-
|
|
23
|
-
`scripts/sync-wsl.sh`、`scripts/deploy-preview.sh` 是开发用的,**不发布**。
|
|
24
|
-
|
|
25
|
-
### 修复:一个不带正文的帧会崩掉握手
|
|
26
|
-
|
|
27
|
-
`ws.ts` 把收到的任何消息一律 `.toString()`,于是 payload 缺失时会得到字符串 `"undefined"`,
|
|
28
|
-
再被当成帧去解析。现在只接受真的带文本的帧(字符串 / Buffer / Buffer[]),
|
|
29
|
-
其余直接忽略,并在握手阶段给出可读原因。
|
|
30
|
-
|
|
31
|
-
## 0.1.0 — 2026-11-07
|
|
32
|
-
|
|
33
|
-
首个公开发布。**一个常驻主 loop,把工单派给多台在线设备上各自完整运行的 DSH,账本只有一份。**
|
|
34
|
-
|
|
35
|
-
### 新增:设备与派单
|
|
36
|
-
|
|
37
|
-
- **设备注册表**(`src/foreman/devices.ts`):同一份 SQLite 里的 `foreman-device:` 家族,
|
|
38
|
-
令牌只存 SHA-256。加设备、换令牌、吊销都不需要重启主 loop。
|
|
39
|
-
CLI:`scripts/add-device.mjs add | list | rotate | revoke`。
|
|
40
|
-
- **Selector**(`src/foreman/selector.ts`):纯函数。吊销的、离线的、没有对应项目 root 的设备先出局;
|
|
41
|
-
显式点名或标签要求再收窄;剩下的按在跑数量最少,平局取最近心跳。
|
|
42
|
-
- **选择落在 accept 上**:`Ticket.target` 在 accept 时确定,`newAssignment` 复制到
|
|
43
|
-
`Assignment.target`。选设备在 `prepareTicket` 缝上,**不在** Executor 里——Executor 没有写库通道。
|
|
44
|
-
- **ForemanExecutor**(`src/foreman/executor.ts`):本机目标原样交给 `DshExecutor`;
|
|
45
|
-
远端目标变成一个 `assign` 帧。设备不可达时报 `blocked`,**不伪造 failed,也不悄悄改成本机执行**。
|
|
46
|
-
- **Foreman Hub**(`src/foreman/hub.ts` + `ws.ts`):设备主动外连,`ws` 只出现在一个文件里,
|
|
47
|
-
只监听 `127.0.0.1`(对外由隧道终结 TLS)。握手校验令牌与协议版本;吊销在下一帧就生效。
|
|
48
|
-
- **文本渲染**(`src/foreman/render.ts`):任务文本在主 loop 侧定稿,**不带任何主 loop 路径**;
|
|
49
|
-
附件走 base64 字节,按编码后大小卡 256 KiB 上限。
|
|
50
|
-
|
|
51
|
-
### 新增:断线与恢复
|
|
52
|
-
|
|
53
|
-
- **断线绝不自动重派**。`assignmentInterrupted` 会走 `Tickets.restart` 立刻重开一张,
|
|
54
|
-
而设备可能还在跑——那是「同一件事执行两遍」。超时只转 `blocked`,交给人。
|
|
55
|
-
- Hub 重启时,在飞的远端 assignment 一律先转 `blocked`,等设备 `hello` 对齐;
|
|
56
|
-
`DshExecutor.resume()` 按 target 跳过远端项。
|
|
57
|
-
- 设备重连后先 `hello` 报「我在跑哪些」,Hub 对齐而不是假设。
|
|
58
|
-
|
|
59
|
-
### 新增:设备侧插件(另一个包)
|
|
60
|
-
|
|
61
|
-
- [`dsh-foreman-node`](https://www.npmjs.com/package/dsh-foreman-node):连出去、接单、回报三件事。
|
|
62
|
-
job 会话是 `job-<assignmentId>`,权限是 **`workspace-write`**(不是主 loop 自己的 `danger-full-access`:
|
|
63
|
-
笔记本前没人,弹审批等于卡死)。
|
|
64
|
-
- 设备上注册 `foreman_report` 工具(`progress` / `ready` / `blocked` / `failed`)。
|
|
65
|
-
**`dropped` 不允许**:那是判断,设备没有判断权。
|
|
66
|
-
|
|
67
|
-
### 新增:面板
|
|
68
|
-
|
|
69
|
-
- 「设置 › 设备」:在线点、在跑数、最近活动、项目名(**不显示设备自己的路径**)、吊销与换令牌。
|
|
70
|
-
|
|
71
|
-
### 新增:飞书群即项目
|
|
72
|
-
|
|
73
|
-
- Policy 的 `routing.chats`:群 → 工作区,同时约束设备选择(内网群只选带 `intranet` 标签的设备)。
|
|
74
|
-
映射按 settings 修订号缓存,改了对新到的消息生效。
|
|
75
|
-
|
|
76
|
-
### 新增:迁移
|
|
77
|
-
|
|
78
|
-
- `scripts/migrate.mjs`:`plan` / `checkpoint` / `pack` / `verify`。
|
|
79
|
-
必须先 checkpoint——库跑在 WAL 模式,只拷 `.sqlite` 会**静默丢掉**最近的工单。
|
|
80
|
-
- [docs/migration.md](docs/migration.md) 五步 runbook。
|
|
81
|
-
|
|
82
|
-
### 依赖
|
|
83
|
-
|
|
84
|
-
- 唯一新增的运行时依赖是 `ws@8`。
|
|
85
|
-
- `@deepseek-ai/*` 全是 `peerDependencies`,由装了本插件的 DSH 实例提供。
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## 0.2.0 — 2026-11-01(上游基线,非本项目发布)
|
|
90
|
-
|
|
91
|
-
> 这一节记录的是代码基线 `dsh-superagent-local` 相对上游 `dsh-superagent-prototype` 的改动。
|
|
92
|
-
> 本项目的 `0.1.0` 已包含其中全部内容。
|
|
5
|
+
## 0.2.0 — 2026-11-01
|
|
93
6
|
|
|
94
7
|
### 新增:需求来自本机操作者
|
|
95
8
|
|
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/cordis.patch.yml
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
name: dsh-foreman
|
|
7
7
|
- id: dsh-foreman-github
|
|
8
8
|
name: dsh-foreman/github-watch
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# 飞书 IM 来源:群消息与私聊进工单。没有配置机器人 CLI 时它自己报「未设置机器人 CLI」,
|
|
10
|
+
# 不影响别的来源,所以默认开着,让人能直接在设置里填。
|
|
11
11
|
- id: dsh-foreman-feishu
|
|
12
12
|
name: dsh-foreman/feishu-watch
|
|
13
|
-
#
|
|
13
|
+
# 本地 Source:把操作者在主会话里说的话当作需求来源;一个实例覆盖所有工作区。
|
|
14
14
|
- id: dsh-foreman-dsh
|
|
15
15
|
name: dsh-foreman/local-watch
|
|
16
16
|
# The agent preset of every Executor Session (WORKER_PRESET in src/app/sessions.ts); the main Session has its own, below.
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
name: '@deepseek-ai/dsh-agent-preset'
|
|
25
25
|
config:
|
|
26
26
|
id: dsh-foreman-worker
|
|
27
|
-
name: dsh-foreman
|
|
28
|
-
description: dsh-foreman
|
|
27
|
+
name: dsh-foreman 执行器
|
|
28
|
+
description: dsh-foreman 的执行器 Session 专用,不供手动选择。前台命令超 5 秒转入后台,只含执行工作所需的工具。
|
|
29
29
|
order: 90
|
|
30
30
|
plugins:
|
|
31
31
|
- id: persona
|
|
@@ -129,8 +129,8 @@
|
|
|
129
129
|
name: '@deepseek-ai/dsh-agent-preset'
|
|
130
130
|
config:
|
|
131
131
|
id: dsh-foreman-router
|
|
132
|
-
name: dsh-foreman
|
|
133
|
-
description: dsh-foreman
|
|
132
|
+
name: dsh-foreman 主 Session
|
|
133
|
+
description: dsh-foreman 的主 Session 专用,不供手动选择。前台命令和任务等待超 5 秒转入后台,只含路由所需的工具。
|
|
134
134
|
order: 91
|
|
135
135
|
plugins:
|
|
136
136
|
- id: agent-instructions
|
package/icon.svg
CHANGED
|
File without changes
|
package/lib/client.js
CHANGED
|
@@ -4598,7 +4598,18 @@ window.__ModuleLoader__.load({
|
|
|
4598
4598
|
devicesOnline: ["在线", "Online"],
|
|
4599
4599
|
devicesRunning: ["在跑", "Running"],
|
|
4600
4600
|
devicesEmpty: ["还没有登记任何设备", "No device is registered yet"],
|
|
4601
|
-
devicesEmptyHint: ["
|
|
4601
|
+
devicesEmptyHint: ["在下面登记一台设备,它会给出三样东西填进那台设备的配置页。", "Register a machine below; it gives you the three values to put on that machine's own configuration page."],
|
|
4602
|
+
devicesAdd: ["登记一台设备", "Register a machine"],
|
|
4603
|
+
devicesAddHint: ["登记后这台设备才能接单。登记不会连上它——连接由那台设备自己发起。", "A machine takes work only after it is registered here. Registering does not connect it: the machine dials out itself."],
|
|
4604
|
+
devicesAddId: ["设备 id", "Device id"],
|
|
4605
|
+
devicesAddName: ["显示名", "Display name"],
|
|
4606
|
+
devicesAddRoots: ["它能接的项目", "Projects it can take"],
|
|
4607
|
+
devicesAddRoot: ["加一个项目", "Add a project"],
|
|
4608
|
+
devicesAddProject: ["项目名", "Project"],
|
|
4609
|
+
devicesAddPath: ["那台机器上的路径", "Path on that machine"],
|
|
4610
|
+
devicesAddSubmit: ["登记", "Register"],
|
|
4611
|
+
devicesAddMinted: ["把这四样填进那台设备的配置页", "Put these values on that machine's configuration page"],
|
|
4612
|
+
devicesAddMintedHint: ["令牌只显示这一次,库里只存哈希。关掉或刷新这一页就再也拿不回来了;丢了只能换一个。", "The token is shown once; only its hash is stored. Closing or refreshing this page loses it for good, and the only way back is a new one."],
|
|
4602
4613
|
deviceOnline: ["在线", "Online"],
|
|
4603
4614
|
deviceRevoked: ["已吊销", "Revoked"],
|
|
4604
4615
|
deviceRunning: ["在跑 {count} 件", "{count} running"],
|
|
@@ -5545,7 +5556,7 @@ window.__ModuleLoader__.load({
|
|
|
5545
5556
|
//#endregion
|
|
5546
5557
|
//#region src/client/devices.tsx
|
|
5547
5558
|
/** Devices settings group: which machines may take work, whether each is online, and what it is running. */
|
|
5548
|
-
/** The device list,
|
|
5559
|
+
/** The device list, the registration form, and the two token actions. */
|
|
5549
5560
|
function DevicesSettings() {
|
|
5550
5561
|
const { t, call } = usePanel();
|
|
5551
5562
|
const { data, error, reload } = usePoll(() => call("GET devices"), []);
|
|
@@ -5585,6 +5596,9 @@ window.__ModuleLoader__.load({
|
|
|
5585
5596
|
})
|
|
5586
5597
|
]
|
|
5587
5598
|
}),
|
|
5599
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddDevice, { onAdded: () => {
|
|
5600
|
+
reload();
|
|
5601
|
+
} }),
|
|
5588
5602
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
5589
5603
|
title: t("devicesList"),
|
|
5590
5604
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListSection, {
|
|
@@ -5697,6 +5711,143 @@ window.__ModuleLoader__.load({
|
|
|
5697
5711
|
})]
|
|
5698
5712
|
});
|
|
5699
5713
|
}
|
|
5714
|
+
/**
|
|
5715
|
+
* Registering a machine, in place.
|
|
5716
|
+
*
|
|
5717
|
+
* The three values the machine needs are shown together and once: the hub keeps only a token hash, so a token that
|
|
5718
|
+
* leaves this card is gone. That is why the card is not dismissible: closing it is losing the token, and the page
|
|
5719
|
+
* says so instead of letting someone find out at the device.
|
|
5720
|
+
*/
|
|
5721
|
+
function AddDevice({ onAdded }) {
|
|
5722
|
+
const { t, call } = usePanel();
|
|
5723
|
+
const { data: state } = usePoll(() => call("GET state"), []);
|
|
5724
|
+
const [id, setId] = (0, react.useState)("");
|
|
5725
|
+
const [name, setName] = (0, react.useState)("");
|
|
5726
|
+
const [roots, setRoots] = (0, react.useState)([]);
|
|
5727
|
+
const [minted, setMinted] = (0, react.useState)();
|
|
5728
|
+
const [refusal, setRefusal] = (0, react.useState)();
|
|
5729
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
5730
|
+
const register = async () => {
|
|
5731
|
+
setBusy(true);
|
|
5732
|
+
setRefusal(void 0);
|
|
5733
|
+
try {
|
|
5734
|
+
const result = await call("POST devices", { body: {
|
|
5735
|
+
id: id.trim(),
|
|
5736
|
+
name: name.trim(),
|
|
5737
|
+
labels: [],
|
|
5738
|
+
roots: roots.map((root) => ({
|
|
5739
|
+
project: root.project.trim(),
|
|
5740
|
+
path: root.path.trim()
|
|
5741
|
+
}))
|
|
5742
|
+
} });
|
|
5743
|
+
setMinted({
|
|
5744
|
+
id: result.id,
|
|
5745
|
+
token: result.token
|
|
5746
|
+
});
|
|
5747
|
+
setId("");
|
|
5748
|
+
setName("");
|
|
5749
|
+
setRoots([]);
|
|
5750
|
+
onAdded();
|
|
5751
|
+
} catch (error) {
|
|
5752
|
+
setRefusal(errorText(error));
|
|
5753
|
+
} finally {
|
|
5754
|
+
setBusy(false);
|
|
5755
|
+
}
|
|
5756
|
+
};
|
|
5757
|
+
const edit = (index, patch) => {
|
|
5758
|
+
setRoots((current) => current.map((root, at) => at === index ? {
|
|
5759
|
+
...root,
|
|
5760
|
+
...patch
|
|
5761
|
+
} : root));
|
|
5762
|
+
};
|
|
5763
|
+
const hubPort = state?.setup.machine.hubPort ?? 0;
|
|
5764
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
5765
|
+
title: t("devicesAdd"),
|
|
5766
|
+
hint: t("devicesAddHint"),
|
|
5767
|
+
children: [
|
|
5768
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(FieldGrid, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5769
|
+
label: t("devicesAddId"),
|
|
5770
|
+
required: true,
|
|
5771
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5772
|
+
value: id,
|
|
5773
|
+
onChange: (event) => setId(event.target.value),
|
|
5774
|
+
placeholder: "laptop"
|
|
5775
|
+
})
|
|
5776
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5777
|
+
label: t("devicesAddName"),
|
|
5778
|
+
required: true,
|
|
5779
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5780
|
+
value: name,
|
|
5781
|
+
onChange: (event) => setName(event.target.value)
|
|
5782
|
+
})
|
|
5783
|
+
})] }),
|
|
5784
|
+
roots.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldGrid, { children: roots.map((root, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5785
|
+
label: index === 0 ? t("devicesAddProject") : `${t("devicesAddProject")} ${index + 1}`,
|
|
5786
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5787
|
+
value: root.project,
|
|
5788
|
+
onChange: (event) => edit(index, { project: event.target.value }),
|
|
5789
|
+
placeholder: "shop"
|
|
5790
|
+
})
|
|
5791
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5792
|
+
label: index === 0 ? t("devicesAddPath") : `${t("devicesAddPath")} ${index + 1}`,
|
|
5793
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5794
|
+
value: root.path,
|
|
5795
|
+
onChange: (event) => edit(index, { path: event.target.value }),
|
|
5796
|
+
placeholder: "F:\\\\dsh\\\\shop"
|
|
5797
|
+
})
|
|
5798
|
+
})] }, index)) }),
|
|
5799
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5800
|
+
className: "dt-form-actions",
|
|
5801
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5802
|
+
size: "sm",
|
|
5803
|
+
onClick: () => setRoots((current) => [...current, {
|
|
5804
|
+
project: "",
|
|
5805
|
+
path: ""
|
|
5806
|
+
}]),
|
|
5807
|
+
children: t("devicesAddRoot")
|
|
5808
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5809
|
+
size: "sm",
|
|
5810
|
+
variant: "primary",
|
|
5811
|
+
busy,
|
|
5812
|
+
disabled: id.trim() === "" || name.trim() === "",
|
|
5813
|
+
onClick: () => {
|
|
5814
|
+
register();
|
|
5815
|
+
},
|
|
5816
|
+
children: t("devicesAddSubmit")
|
|
5817
|
+
})]
|
|
5818
|
+
}),
|
|
5819
|
+
refusal !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorLine, {
|
|
5820
|
+
error: refusal,
|
|
5821
|
+
title: t("loadFailed")
|
|
5822
|
+
})
|
|
5823
|
+
]
|
|
5824
|
+
}), minted && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
5825
|
+
title: t("devicesAddMinted"),
|
|
5826
|
+
tone: "warn",
|
|
5827
|
+
children: [
|
|
5828
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, { children: t("devicesAddMintedHint") }),
|
|
5829
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValues, { rows: mintedRows(minted, hubPort) }),
|
|
5830
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
5831
|
+
className: "dt-mono",
|
|
5832
|
+
"data-device-token": true,
|
|
5833
|
+
children: minted.token
|
|
5834
|
+
})
|
|
5835
|
+
]
|
|
5836
|
+
})] });
|
|
5837
|
+
}
|
|
5838
|
+
/**
|
|
5839
|
+
* The rows a device needs, with the hub's real port.
|
|
5840
|
+
*
|
|
5841
|
+
* A hub with `hubPort: 0` has its listener switched off, so no address a card could print would work; saying so is
|
|
5842
|
+
* more use than a plausible-looking port that nothing listens on.
|
|
5843
|
+
*/
|
|
5844
|
+
function mintedRows(minted, hubPort) {
|
|
5845
|
+
return [
|
|
5846
|
+
["url", hubPort > 0 ? `ws://127.0.0.1:${hubPort}` : "the hub is off: set hubPort in the plugin configuration"],
|
|
5847
|
+
["deviceId", minted.id],
|
|
5848
|
+
["token", minted.token]
|
|
5849
|
+
];
|
|
5850
|
+
}
|
|
5700
5851
|
//#endregion
|
|
5701
5852
|
//#region src/client/logs.tsx
|
|
5702
5853
|
/**
|
package/lib/index.js
CHANGED
|
@@ -8802,6 +8802,10 @@ var RouteError = class extends Error {
|
|
|
8802
8802
|
this.status = status;
|
|
8803
8803
|
}
|
|
8804
8804
|
};
|
|
8805
|
+
/** Whether a registration failed because the id is taken, the only failure that is the caller's to fix. */
|
|
8806
|
+
function isDuplicateDevice(error) {
|
|
8807
|
+
return error instanceof Error && /is already registered/.test(error.message);
|
|
8808
|
+
}
|
|
8805
8809
|
async function readBody(req, maxBytes = 131072) {
|
|
8806
8810
|
let size = 0;
|
|
8807
8811
|
const chunks = [];
|
|
@@ -8924,12 +8928,13 @@ function failure(error, internal) {
|
|
|
8924
8928
|
id
|
|
8925
8929
|
}];
|
|
8926
8930
|
}
|
|
8927
|
-
function machineView({ stateDir, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs }) {
|
|
8931
|
+
function machineView({ stateDir, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs, hubPort }) {
|
|
8928
8932
|
return {
|
|
8929
8933
|
stateDir,
|
|
8930
8934
|
promptMaxChars: promptMaxChars.get(),
|
|
8931
8935
|
admissionContextMaxPages: admissionContextMaxPages.get(),
|
|
8932
|
-
admissionContextTimeoutMs: admissionContextTimeoutMs.get()
|
|
8936
|
+
admissionContextTimeoutMs: admissionContextTimeoutMs.get(),
|
|
8937
|
+
hubPort
|
|
8933
8938
|
};
|
|
8934
8939
|
}
|
|
8935
8940
|
/** Every setup problem the panel shows: global first, then the ones that pause a single Source, each with that Source's title. */
|
|
@@ -9169,6 +9174,24 @@ function routes(runtime) {
|
|
|
9169
9174
|
devices: runtime.devices(),
|
|
9170
9175
|
online: runtime.onlineDevices()
|
|
9171
9176
|
}),
|
|
9177
|
+
/** 登记一台新设备。令牌只在这一次响应里出现,刷新就没了。 */
|
|
9178
|
+
"POST devices": async (input) => {
|
|
9179
|
+
const body = z.object({
|
|
9180
|
+
id: z.string().trim().min(1),
|
|
9181
|
+
name: z.string().trim().min(1),
|
|
9182
|
+
labels: z.array(z.string().trim().min(1)).optional(),
|
|
9183
|
+
roots: z.array(z.object({
|
|
9184
|
+
project: z.string().trim().min(1),
|
|
9185
|
+
path: z.string().trim().min(1)
|
|
9186
|
+
}))
|
|
9187
|
+
}).parse(await input.body());
|
|
9188
|
+
try {
|
|
9189
|
+
return runtime.addDevice(body);
|
|
9190
|
+
} catch (error) {
|
|
9191
|
+
if (isDuplicateDevice(error)) throw new RouteError(400, error.message);
|
|
9192
|
+
throw error;
|
|
9193
|
+
}
|
|
9194
|
+
},
|
|
9172
9195
|
/** 吊销一台设备:登记保留(审计要看),下一次心跳就断开。 */
|
|
9173
9196
|
"POST devices/:id/revoke": ({ params }) => {
|
|
9174
9197
|
runtime.revokeDevice(decodeURIComponent(params.id));
|
|
@@ -12376,6 +12399,14 @@ var Runtime = class extends Service {
|
|
|
12376
12399
|
models: () => this.ctx.sessionController.modelCatalog(),
|
|
12377
12400
|
devices: () => runtime.devices.list(),
|
|
12378
12401
|
onlineDevices: () => runtime.hub?.snapshot() ?? [],
|
|
12402
|
+
/** 登记一台设备并铸一枚令牌;和 `scripts/add-device.mjs` 走同一个 `Devices.add`。 */
|
|
12403
|
+
addDevice: (input) => {
|
|
12404
|
+
const { device, token } = runtime.devices.add(input);
|
|
12405
|
+
return {
|
|
12406
|
+
id: device.id,
|
|
12407
|
+
token
|
|
12408
|
+
};
|
|
12409
|
+
},
|
|
12379
12410
|
revokeDevice: (id) => {
|
|
12380
12411
|
runtime.devices.revoke(id);
|
|
12381
12412
|
runtime.hub?.detach(id);
|
package/locale/en.json
CHANGED
|
File without changes
|
package/locale/zh.json
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-foreman",
|
|
3
3
|
"author": "niechao136",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"description": "dsh-foreman:主 loop 在多设备上派单的 DSH bundle:需求来自本机操作者的主会话而不是 GitHub/飞书,每个工作区各有一条主会话,主会话只负责路由,真正的活儿派给 Worker 在该工作区目录里执行。",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "lib/index.js",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"packageManager": "pnpm@11.7.0",
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"dsh-foreman-node": "0.
|
|
65
|
+
"dsh-foreman-node": "0.2.0",
|
|
66
66
|
"proper-lockfile": "4.1.2",
|
|
67
67
|
"ws": "8.22.0",
|
|
68
68
|
"zod": "4.4.3"
|
package/prompts/admission.md
CHANGED
|
@@ -28,7 +28,6 @@ The host posts it as a reply to the triggering message; do not quote that messag
|
|
|
28
28
|
Return rule_assessments with exactly one entry for every enabled rule: rule_id, matched and a factual reason.
|
|
29
29
|
Include negative judgments as matched=false; a reason saying that a rule does not apply cannot be a positive match.
|
|
30
30
|
Never include disabled, unknown or repeated rule IDs.
|
|
31
|
-
Fill device only when the request itself says where the work has to happen — a named machine, an office-only system, a machine the person says they are at. Otherwise leave it out: the host picks the least busy online machine that can reach the project, and a guess here can send work to a machine that cannot do it.
|
|
32
31
|
The host derives positive-only matches from those booleans.
|
|
33
32
|
One source message produces at most one ticket.
|
|
34
33
|
If matching rules conflict or the scope is ambiguous, ignore quietly.
|
package/prompts/cli.md
CHANGED
|
File without changes
|
package/prompts/dispatch.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/prompts/handoff.md
CHANGED
|
File without changes
|
package/prompts/orchestrator.md
CHANGED
|
File without changes
|
package/prompts/replies.md
CHANGED
|
File without changes
|
package/prompts/shared.md
CHANGED
|
File without changes
|
package/prompts/subscriptions.md
CHANGED
|
File without changes
|
|
File without changes
|
package/prompts/team/delivery.md
CHANGED
|
File without changes
|
package/prompts/team/scope.md
CHANGED
|
File without changes
|
package/prompts/work.md
CHANGED
|
File without changes
|
package/prompts/worker.md
CHANGED
|
File without changes
|
package/scripts/add-device.mjs
CHANGED
|
File without changes
|
package/scripts/dsh-foreman
CHANGED
|
File without changes
|
package/scripts/migrate.mjs
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|