dsh-foreman 0.1.1 → 0.3.1
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 +61 -0
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/cordis.patch.yml +7 -7
- package/icon.svg +0 -0
- package/lib/client.js +350 -33
- package/lib/feishu-watch.d.ts +2 -2
- package/lib/github-watch.d.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +135 -5
- package/lib/{intake-Di3PgVj2.d.ts → intake-B1UsoHuw.d.ts} +8 -0
- package/lib/{kind-DGutnJ2b.d.ts → kind-BYtP14oM.d.ts} +1 -1
- package/lib/local-watch.d.ts +2 -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
|
@@ -6,6 +6,67 @@
|
|
|
6
6
|
|
|
7
7
|
本项目自己的版本从 `0.1.0` 开始。
|
|
8
8
|
|
|
9
|
+
## 0.3.1 — 2026-11-08
|
|
10
|
+
|
|
11
|
+
### 修复:同一 id 的两个连接互相踢,设备永远上不了线
|
|
12
|
+
|
|
13
|
+
`Hub.attach` 在新连接到达时,立刻关闭同 id 的旧连接。设备重连时旧 socket 往往还没超时,
|
|
14
|
+
于是**两边互相踢**:谁都完不成握手,hub 一直认为设备离线,双方不停重试。
|
|
15
|
+
|
|
16
|
+
现场证据:本机 89 个 TIME_WAIT,hub 上一个在线设备都没有;一张要查看宿舍电脑的工单,
|
|
17
|
+
因为选不到设备,回退到服务器本地,在空目录里跑完并报告成功。
|
|
18
|
+
|
|
19
|
+
现在新连接先排队,**令牌验证通过后才接管 id**。半成品连接打扰不到正在工作的那条,
|
|
20
|
+
真正的重连仍然能接管。
|
|
21
|
+
|
|
22
|
+
### 修复:点名了机器,却在服务器上跑
|
|
23
|
+
|
|
24
|
+
`prepareTicket` 在选不到设备时静默回退本地执行,并把这种行为写成了「有意为之」。
|
|
25
|
+
在「项目只存在于远程设备」的场景里,它给出一个看起来成功、实际毫无意义的结果。
|
|
26
|
+
|
|
27
|
+
现在:点名了设备或要求了标签的请求,选不到设备就记 `blockedReason`,由 executor
|
|
28
|
+
直接报 `blocked`,**不启动任何 Session**。人在面板上看到「没有设备能接」,
|
|
29
|
+
而不是假成功。没点名设备的仍然回退本地——本机可达的项目不该因为笔记本离线就卡住。
|
|
30
|
+
|
|
31
|
+
### 修复:worker 会话在侧栏看不到
|
|
32
|
+
|
|
33
|
+
原来把所有 `worker-*` 都藏起来。那恰好藏掉了唯一能回答「任务在哪台机器、哪个目录跑」的线索。
|
|
34
|
+
现在只藏主会话(用行上的 M 气泡进),worker 保留显示。
|
|
35
|
+
|
|
36
|
+
### 新增:设备可以删除,也可以改项目
|
|
37
|
+
|
|
38
|
+
- **删除**:吊销保留记录(审计用),删除连记录一起去掉(登记错了用)。删除需要二次确认。
|
|
39
|
+
- **编辑**:显示名、标签、它能接的项目都能改,**id 和令牌不动**,所以那台设备不用重新配置。
|
|
40
|
+
这解决了「登记时漏了项目只能重登」的问题。
|
|
41
|
+
|
|
42
|
+
## 0.3.0 — 2026-11-07
|
|
43
|
+
|
|
44
|
+
### 新增:在面板里登记设备,不用再跑脚本
|
|
45
|
+
|
|
46
|
+
「设置 › 设备」多了一个登记表单:填 id、显示名、它能接的项目和**那台机器上的路径**,
|
|
47
|
+
点「登记」就得到一枚令牌。卡片把设备要的四样东西排在一起——`url`、`deviceId`、`token`,
|
|
48
|
+
以及设备页面的入口——令牌只显示这一次,因为库里只存哈希,关掉或刷新就再也拿不回来。
|
|
49
|
+
|
|
50
|
+
以前这一步只能跑 `scripts/add-device.mjs`,那个脚本还在,行为不变;面板走的是同一个
|
|
51
|
+
`Devices.add`,所以两条路得到的结果一样。
|
|
52
|
+
|
|
53
|
+
重复的 id 会回 400 并明确说「去换令牌」,不是 500——那是操作者的失误,不是服务端故障。
|
|
54
|
+
|
|
55
|
+
### 新增接口
|
|
56
|
+
|
|
57
|
+
`POST /orchestrator/api/devices`,body `{ id, name, labels?, roots }`,
|
|
58
|
+
响应 `{ id, token }`。令牌只在这里出现一次。
|
|
59
|
+
|
|
60
|
+
### 修复:bundle patch 里的中文是乱码
|
|
61
|
+
|
|
62
|
+
`cordis.patch.yml` 里几行中文在初始提交时就已经是「UTF-8 字节被当成 GBK 读」的产物,
|
|
63
|
+
所以面板上主会话旁边显示的是 `dsh-foreman 涓?Session`。已改回正确的
|
|
64
|
+
`dsh-foreman 主 Session` 与 `dsh-foreman 执行器`。
|
|
65
|
+
|
|
66
|
+
### 依赖
|
|
67
|
+
|
|
68
|
+
`dsh-foreman-node` 精确锁到 `0.2.0`(不写 `^`):两边协议必须同版本,自动升级反而危险。
|
|
69
|
+
|
|
9
70
|
## 0.1.1 — 2026-11-07
|
|
10
71
|
|
|
11
72
|
修的是 `0.1.0` 发布时才暴露的两个问题,都是真机验证(上海服务器 srv-02)发现的。
|
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,11 +4598,29 @@ 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"],
|
|
4605
4616
|
deviceLastSeen: ["最近活动", "Last seen"],
|
|
4617
|
+
deviceEdit: ["编辑", "Edit"],
|
|
4618
|
+
deviceEditHint: ["改显示名、标签和能接的项目。id 和令牌不在这里改:id 已经写在那台机器的配置里,换令牌要先把新令牌给它。", "Change the display name, labels and projects. The id and token are not editable here: the id is already in that machine's configuration, and a new token must be handed to it first."],
|
|
4619
|
+
deviceLabels: ["标签", "Labels"],
|
|
4620
|
+
deviceLabelsHint: ["逗号分隔。群聊可以要求某个标签,只有带这个标签的设备才接得到。", "Comma separated. A chat can require a label, so only devices carrying it can take that work."],
|
|
4621
|
+
devicePathKeep: ["留空即保持原路径", "Leave empty to keep the current path"],
|
|
4622
|
+
deviceDelete: ["删除", "Delete"],
|
|
4623
|
+
deviceDeleteConfirm: ["确认删除", "Confirm delete"],
|
|
4606
4624
|
deviceRevoke: ["吊销", "Revoke"],
|
|
4607
4625
|
deviceRevokeConfirm: ["确认吊销", "Confirm revoke"],
|
|
4608
4626
|
deviceRotate: ["换令牌", "New token"],
|
|
@@ -5545,11 +5563,12 @@ window.__ModuleLoader__.load({
|
|
|
5545
5563
|
//#endregion
|
|
5546
5564
|
//#region src/client/devices.tsx
|
|
5547
5565
|
/** Devices settings group: which machines may take work, whether each is online, and what it is running. */
|
|
5548
|
-
/** The device list,
|
|
5566
|
+
/** The device list, the registration form, and the two token actions. */
|
|
5549
5567
|
function DevicesSettings() {
|
|
5550
5568
|
const { t, call } = usePanel();
|
|
5551
5569
|
const { data, error, reload } = usePoll(() => call("GET devices"), []);
|
|
5552
5570
|
const [rotated, setRotated] = (0, react.useState)();
|
|
5571
|
+
const [editing, setEditing] = (0, react.useState)();
|
|
5553
5572
|
const revoke = async (id) => {
|
|
5554
5573
|
await call("POST devices/:id/revoke", { id });
|
|
5555
5574
|
await reload();
|
|
@@ -5561,6 +5580,10 @@ window.__ModuleLoader__.load({
|
|
|
5561
5580
|
});
|
|
5562
5581
|
await reload();
|
|
5563
5582
|
};
|
|
5583
|
+
const remove = async (id) => {
|
|
5584
|
+
await call("DELETE devices/:id", { id });
|
|
5585
|
+
await reload();
|
|
5586
|
+
};
|
|
5564
5587
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5565
5588
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageHead, {
|
|
5566
5589
|
back: false,
|
|
@@ -5585,6 +5608,9 @@ window.__ModuleLoader__.load({
|
|
|
5585
5608
|
})
|
|
5586
5609
|
]
|
|
5587
5610
|
}),
|
|
5611
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddDevice, { onAdded: () => {
|
|
5612
|
+
reload();
|
|
5613
|
+
} }),
|
|
5588
5614
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
5589
5615
|
title: t("devicesList"),
|
|
5590
5616
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListSection, {
|
|
@@ -5601,11 +5627,20 @@ window.__ModuleLoader__.load({
|
|
|
5601
5627
|
children: data.rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceLine, {
|
|
5602
5628
|
row,
|
|
5603
5629
|
onRevoke: () => revoke(row.id),
|
|
5604
|
-
onRotate: () => rotate(row.id)
|
|
5630
|
+
onRotate: () => rotate(row.id),
|
|
5631
|
+
onEdit: () => setEditing(row),
|
|
5632
|
+
onDelete: () => remove(row.id)
|
|
5605
5633
|
}, row.id))
|
|
5606
5634
|
}) : void 0
|
|
5607
5635
|
})
|
|
5608
5636
|
}),
|
|
5637
|
+
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EditDevice, {
|
|
5638
|
+
row: editing,
|
|
5639
|
+
onClose: () => setEditing(void 0),
|
|
5640
|
+
onSaved: () => {
|
|
5641
|
+
reload();
|
|
5642
|
+
}
|
|
5643
|
+
}),
|
|
5609
5644
|
rotated && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
5610
5645
|
title: t("devicesRotated"),
|
|
5611
5646
|
tone: "warn",
|
|
@@ -5624,10 +5659,11 @@ window.__ModuleLoader__.load({
|
|
|
5624
5659
|
})
|
|
5625
5660
|
] });
|
|
5626
5661
|
}
|
|
5627
|
-
/** One device: name, online dot, load, project names, last seen, and the
|
|
5628
|
-
function DeviceLine({ row, onRevoke, onRotate }) {
|
|
5662
|
+
/** One device: name, online dot, load, project names, last seen, and the actions an operator needs. */
|
|
5663
|
+
function DeviceLine({ row, onRevoke, onRotate, onEdit, onDelete }) {
|
|
5629
5664
|
const { t } = usePanel();
|
|
5630
5665
|
const [confirming, setConfirming] = (0, react.useState)(false);
|
|
5666
|
+
const [deleting, setDeleting] = (0, react.useState)(false);
|
|
5631
5667
|
const tone = row.revoked ? "idle" : row.online ? "done" : "warning";
|
|
5632
5668
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
5633
5669
|
className: "dt-row",
|
|
@@ -5669,34 +5705,310 @@ window.__ModuleLoader__.load({
|
|
|
5669
5705
|
})]
|
|
5670
5706
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5671
5707
|
className: "dt-row-actions",
|
|
5672
|
-
children: [
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
},
|
|
5679
|
-
children:
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5708
|
+
children: [
|
|
5709
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5710
|
+
variant: "ghost",
|
|
5711
|
+
size: "sm",
|
|
5712
|
+
onClick: onEdit,
|
|
5713
|
+
children: t("deviceEdit")
|
|
5714
|
+
}),
|
|
5715
|
+
!row.revoked && (confirming ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5716
|
+
variant: "danger",
|
|
5717
|
+
size: "sm",
|
|
5718
|
+
onClick: () => {
|
|
5719
|
+
setConfirming(false);
|
|
5720
|
+
onRevoke();
|
|
5721
|
+
},
|
|
5722
|
+
children: t("deviceRevokeConfirm")
|
|
5723
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5724
|
+
size: "sm",
|
|
5725
|
+
onClick: () => setConfirming(false),
|
|
5726
|
+
children: t("cancel")
|
|
5727
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5728
|
+
variant: "ghost",
|
|
5729
|
+
size: "sm",
|
|
5730
|
+
onClick: () => setConfirming(true),
|
|
5731
|
+
children: t("deviceRevoke")
|
|
5732
|
+
})),
|
|
5733
|
+
!row.revoked && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5734
|
+
variant: "ghost",
|
|
5735
|
+
size: "sm",
|
|
5736
|
+
onClick: () => {
|
|
5737
|
+
onRotate();
|
|
5738
|
+
},
|
|
5739
|
+
children: t("deviceRotate")
|
|
5740
|
+
}),
|
|
5741
|
+
deleting ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5742
|
+
variant: "danger",
|
|
5743
|
+
size: "sm",
|
|
5744
|
+
onClick: () => {
|
|
5745
|
+
setDeleting(false);
|
|
5746
|
+
onDelete();
|
|
5747
|
+
},
|
|
5748
|
+
children: t("deviceDeleteConfirm")
|
|
5749
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5750
|
+
size: "sm",
|
|
5751
|
+
onClick: () => setDeleting(false),
|
|
5752
|
+
children: t("cancel")
|
|
5753
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5754
|
+
variant: "ghost",
|
|
5755
|
+
size: "sm",
|
|
5756
|
+
onClick: () => setDeleting(true),
|
|
5757
|
+
children: t("deviceDelete")
|
|
5758
|
+
})
|
|
5759
|
+
]
|
|
5697
5760
|
})]
|
|
5698
5761
|
});
|
|
5699
5762
|
}
|
|
5763
|
+
/**
|
|
5764
|
+
* Registering a machine, in place.
|
|
5765
|
+
*
|
|
5766
|
+
* The three values the machine needs are shown together and once: the hub keeps only a token hash, so a token that
|
|
5767
|
+
* leaves this card is gone. That is why the card is not dismissible: closing it is losing the token, and the page
|
|
5768
|
+
* says so instead of letting someone find out at the device.
|
|
5769
|
+
*/
|
|
5770
|
+
function AddDevice({ onAdded }) {
|
|
5771
|
+
const { t, call } = usePanel();
|
|
5772
|
+
const { data: state } = usePoll(() => call("GET state"), []);
|
|
5773
|
+
const [id, setId] = (0, react.useState)("");
|
|
5774
|
+
const [name, setName] = (0, react.useState)("");
|
|
5775
|
+
const [roots, setRoots] = (0, react.useState)([]);
|
|
5776
|
+
const [minted, setMinted] = (0, react.useState)();
|
|
5777
|
+
const [refusal, setRefusal] = (0, react.useState)();
|
|
5778
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
5779
|
+
const register = async () => {
|
|
5780
|
+
setBusy(true);
|
|
5781
|
+
setRefusal(void 0);
|
|
5782
|
+
try {
|
|
5783
|
+
const result = await call("POST devices", { body: {
|
|
5784
|
+
id: id.trim(),
|
|
5785
|
+
name: name.trim(),
|
|
5786
|
+
labels: [],
|
|
5787
|
+
roots: roots.map((root) => ({
|
|
5788
|
+
project: root.project.trim(),
|
|
5789
|
+
path: root.path.trim()
|
|
5790
|
+
}))
|
|
5791
|
+
} });
|
|
5792
|
+
setMinted({
|
|
5793
|
+
id: result.id,
|
|
5794
|
+
token: result.token
|
|
5795
|
+
});
|
|
5796
|
+
setId("");
|
|
5797
|
+
setName("");
|
|
5798
|
+
setRoots([]);
|
|
5799
|
+
onAdded();
|
|
5800
|
+
} catch (error) {
|
|
5801
|
+
setRefusal(errorText(error));
|
|
5802
|
+
} finally {
|
|
5803
|
+
setBusy(false);
|
|
5804
|
+
}
|
|
5805
|
+
};
|
|
5806
|
+
const edit = (index, patch) => {
|
|
5807
|
+
setRoots((current) => current.map((root, at) => at === index ? {
|
|
5808
|
+
...root,
|
|
5809
|
+
...patch
|
|
5810
|
+
} : root));
|
|
5811
|
+
};
|
|
5812
|
+
const hubPort = state?.setup.machine.hubPort ?? 0;
|
|
5813
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
5814
|
+
title: t("devicesAdd"),
|
|
5815
|
+
hint: t("devicesAddHint"),
|
|
5816
|
+
children: [
|
|
5817
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(FieldGrid, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5818
|
+
label: t("devicesAddId"),
|
|
5819
|
+
required: true,
|
|
5820
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5821
|
+
value: id,
|
|
5822
|
+
onChange: (event) => setId(event.target.value),
|
|
5823
|
+
placeholder: "laptop"
|
|
5824
|
+
})
|
|
5825
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5826
|
+
label: t("devicesAddName"),
|
|
5827
|
+
required: true,
|
|
5828
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5829
|
+
value: name,
|
|
5830
|
+
onChange: (event) => setName(event.target.value)
|
|
5831
|
+
})
|
|
5832
|
+
})] }),
|
|
5833
|
+
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, {
|
|
5834
|
+
label: index === 0 ? t("devicesAddProject") : `${t("devicesAddProject")} ${index + 1}`,
|
|
5835
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5836
|
+
value: root.project,
|
|
5837
|
+
onChange: (event) => edit(index, { project: event.target.value }),
|
|
5838
|
+
placeholder: "shop"
|
|
5839
|
+
})
|
|
5840
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5841
|
+
label: index === 0 ? t("devicesAddPath") : `${t("devicesAddPath")} ${index + 1}`,
|
|
5842
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5843
|
+
value: root.path,
|
|
5844
|
+
onChange: (event) => edit(index, { path: event.target.value }),
|
|
5845
|
+
placeholder: "F:\\\\dsh\\\\shop"
|
|
5846
|
+
})
|
|
5847
|
+
})] }, index)) }),
|
|
5848
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5849
|
+
className: "dt-form-actions",
|
|
5850
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5851
|
+
size: "sm",
|
|
5852
|
+
onClick: () => setRoots((current) => [...current, {
|
|
5853
|
+
project: "",
|
|
5854
|
+
path: ""
|
|
5855
|
+
}]),
|
|
5856
|
+
children: t("devicesAddRoot")
|
|
5857
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5858
|
+
size: "sm",
|
|
5859
|
+
variant: "primary",
|
|
5860
|
+
busy,
|
|
5861
|
+
disabled: id.trim() === "" || name.trim() === "",
|
|
5862
|
+
onClick: () => {
|
|
5863
|
+
register();
|
|
5864
|
+
},
|
|
5865
|
+
children: t("devicesAddSubmit")
|
|
5866
|
+
})]
|
|
5867
|
+
}),
|
|
5868
|
+
refusal !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorLine, {
|
|
5869
|
+
error: refusal,
|
|
5870
|
+
title: t("loadFailed")
|
|
5871
|
+
})
|
|
5872
|
+
]
|
|
5873
|
+
}), minted && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
5874
|
+
title: t("devicesAddMinted"),
|
|
5875
|
+
tone: "warn",
|
|
5876
|
+
children: [
|
|
5877
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Hint, { children: t("devicesAddMintedHint") }),
|
|
5878
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValues, { rows: mintedRows(minted, hubPort) }),
|
|
5879
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
5880
|
+
className: "dt-mono",
|
|
5881
|
+
"data-device-token": true,
|
|
5882
|
+
children: minted.token
|
|
5883
|
+
})
|
|
5884
|
+
]
|
|
5885
|
+
})] });
|
|
5886
|
+
}
|
|
5887
|
+
/**
|
|
5888
|
+
* The rows a device needs, with the hub's real port.
|
|
5889
|
+
*
|
|
5890
|
+
* A hub with `hubPort: 0` has its listener switched off, so no address a card could print would work; saying so is
|
|
5891
|
+
* more use than a plausible-looking port that nothing listens on.
|
|
5892
|
+
*/
|
|
5893
|
+
function mintedRows(minted, hubPort) {
|
|
5894
|
+
return [
|
|
5895
|
+
["url", hubPort > 0 ? `ws://127.0.0.1:${hubPort}` : "the hub is off: set hubPort in the plugin configuration"],
|
|
5896
|
+
["deviceId", minted.id],
|
|
5897
|
+
["token", minted.token]
|
|
5898
|
+
];
|
|
5899
|
+
}
|
|
5900
|
+
/**
|
|
5901
|
+
* Editing what a device offers: its display name, its labels, and the projects it can work on.
|
|
5902
|
+
*
|
|
5903
|
+
* The id and the token are not editable here on purpose. The id is what the device's own configuration already says,
|
|
5904
|
+
* and changing it would need the machine in front of you; a token is rotated deliberately, because the machine must
|
|
5905
|
+
* be given the new one before it can connect again.
|
|
5906
|
+
*/
|
|
5907
|
+
function EditDevice({ row, onClose, onSaved }) {
|
|
5908
|
+
const { t, call } = usePanel();
|
|
5909
|
+
const [name, setName] = (0, react.useState)(row.name);
|
|
5910
|
+
const [labels, setLabels] = (0, react.useState)(row.labels.join(", "));
|
|
5911
|
+
const [roots, setRoots] = (0, react.useState)(row.projects.map((project) => ({
|
|
5912
|
+
project,
|
|
5913
|
+
path: ""
|
|
5914
|
+
})));
|
|
5915
|
+
const [refusal, setRefusal] = (0, react.useState)();
|
|
5916
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
5917
|
+
const save = async () => {
|
|
5918
|
+
setBusy(true);
|
|
5919
|
+
setRefusal(void 0);
|
|
5920
|
+
try {
|
|
5921
|
+
await call("PUT devices/:id", {
|
|
5922
|
+
id: row.id,
|
|
5923
|
+
body: {
|
|
5924
|
+
name: name.trim(),
|
|
5925
|
+
labels: labels.split(",").map((label) => label.trim()).filter(Boolean),
|
|
5926
|
+
roots: roots.filter((root) => root.project.trim() && root.path.trim()).map((root) => ({
|
|
5927
|
+
project: root.project.trim(),
|
|
5928
|
+
path: root.path.trim()
|
|
5929
|
+
}))
|
|
5930
|
+
}
|
|
5931
|
+
});
|
|
5932
|
+
onSaved();
|
|
5933
|
+
onClose();
|
|
5934
|
+
} catch (error) {
|
|
5935
|
+
setRefusal(errorText(error));
|
|
5936
|
+
} finally {
|
|
5937
|
+
setBusy(false);
|
|
5938
|
+
}
|
|
5939
|
+
};
|
|
5940
|
+
const edit = (index, patch) => setRoots((current) => current.map((root, at) => at === index ? {
|
|
5941
|
+
...root,
|
|
5942
|
+
...patch
|
|
5943
|
+
} : root));
|
|
5944
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
|
|
5945
|
+
title: `${t("deviceEdit")} · ${row.id}`,
|
|
5946
|
+
hint: t("deviceEditHint"),
|
|
5947
|
+
children: [
|
|
5948
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(FieldGrid, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5949
|
+
label: t("devicesAddName"),
|
|
5950
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5951
|
+
value: name,
|
|
5952
|
+
onChange: (event) => setName(event.target.value)
|
|
5953
|
+
})
|
|
5954
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5955
|
+
label: t("deviceLabels"),
|
|
5956
|
+
note: t("deviceLabelsHint"),
|
|
5957
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5958
|
+
value: labels,
|
|
5959
|
+
onChange: (event) => setLabels(event.target.value),
|
|
5960
|
+
placeholder: "intranet, office"
|
|
5961
|
+
})
|
|
5962
|
+
})] }),
|
|
5963
|
+
/* @__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, {
|
|
5964
|
+
label: `${t("devicesAddProject")} ${index + 1}`,
|
|
5965
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5966
|
+
value: root.project,
|
|
5967
|
+
onChange: (event) => edit(index, { project: event.target.value })
|
|
5968
|
+
})
|
|
5969
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
5970
|
+
label: `${t("devicesAddPath")} ${index + 1}`,
|
|
5971
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
|
|
5972
|
+
value: root.path,
|
|
5973
|
+
onChange: (event) => edit(index, { path: event.target.value }),
|
|
5974
|
+
placeholder: t("devicePathKeep")
|
|
5975
|
+
})
|
|
5976
|
+
})] }, index)) }),
|
|
5977
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5978
|
+
className: "dt-form-actions",
|
|
5979
|
+
children: [
|
|
5980
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5981
|
+
size: "sm",
|
|
5982
|
+
onClick: () => setRoots((current) => [...current, {
|
|
5983
|
+
project: "",
|
|
5984
|
+
path: ""
|
|
5985
|
+
}]),
|
|
5986
|
+
"data-edit-root": "1",
|
|
5987
|
+
children: t("devicesAddRoot")
|
|
5988
|
+
}),
|
|
5989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5990
|
+
size: "sm",
|
|
5991
|
+
variant: "primary",
|
|
5992
|
+
busy,
|
|
5993
|
+
onClick: () => {
|
|
5994
|
+
save();
|
|
5995
|
+
},
|
|
5996
|
+
children: t("save")
|
|
5997
|
+
}),
|
|
5998
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
5999
|
+
size: "sm",
|
|
6000
|
+
onClick: onClose,
|
|
6001
|
+
children: t("cancel")
|
|
6002
|
+
})
|
|
6003
|
+
]
|
|
6004
|
+
}),
|
|
6005
|
+
refusal !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorLine, {
|
|
6006
|
+
error: refusal,
|
|
6007
|
+
title: t("loadFailed")
|
|
6008
|
+
})
|
|
6009
|
+
]
|
|
6010
|
+
});
|
|
6011
|
+
}
|
|
5700
6012
|
//#endregion
|
|
5701
6013
|
//#region src/client/logs.tsx
|
|
5702
6014
|
/**
|
|
@@ -8176,10 +8488,15 @@ window.__ModuleLoader__.load({
|
|
|
8176
8488
|
return key.startsWith("workspace:") ? key.slice(10) : void 0;
|
|
8177
8489
|
};
|
|
8178
8490
|
/**
|
|
8179
|
-
*
|
|
8180
|
-
*
|
|
8491
|
+
* 插件自己建的会话里,只有主会话被藏起来。
|
|
8492
|
+
*
|
|
8493
|
+
* `orchestrator-` 是主会话本身,人用行上的 M 气泡进,侧栏再列一行就是重复。
|
|
8494
|
+
*
|
|
8495
|
+
* `worker-` 以前也藏,理由是「数量会多」。但那让人失去唯一的线索:任务到底在哪台机器、哪个目录里跑。
|
|
8496
|
+
* 一张工单派到别的设备时,设备上那条会话就是答案,而它恰恰被藏掉了。现在保留显示:多一行,
|
|
8497
|
+
* 换来看得见工作发生的地方。
|
|
8181
8498
|
*/
|
|
8182
|
-
const ours = (id) => id.startsWith("
|
|
8499
|
+
const ours = (id) => id.startsWith("orchestrator-");
|
|
8183
8500
|
const rowId = (row) => (row.getAttribute("data-row-key") ?? "").replace(/^session:/, "");
|
|
8184
8501
|
/**
|
|
8185
8502
|
* 收起插件自己的会话行,以及状态目录那一个伪工作区行。
|
package/lib/feishu-watch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { G as Trigger, L as Thread } from "./intake-
|
|
2
|
-
import { g as Tally, h as SourceRegistry, n as SourceContext, t as SourceKind } from "./kind-
|
|
1
|
+
import { G as Trigger, L as Thread } from "./intake-B1UsoHuw.js";
|
|
2
|
+
import { g as Tally, h as SourceRegistry, n as SourceContext, t as SourceKind } from "./kind-BYtP14oM.js";
|
|
3
3
|
import { Context } from "@deepseek-ai/cordis";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
//#region src/sources/feishu-read.d.ts
|
package/lib/github-watch.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as apply, c as name, d as pollThreads, f as pollTicketPulls, i as Retry, l as pollFailure, m as commentTrigger, o as githubKind, p as GithubComment, r as GithubConfig, s as inject, u as pollRound } from "./kind-
|
|
1
|
+
import { a as apply, c as name, d as pollThreads, f as pollTicketPulls, i as Retry, l as pollFailure, m as commentTrigger, o as githubKind, p as GithubComment, r as GithubConfig, s as inject, u as pollRound } from "./kind-BYtP14oM.js";
|
|
2
2
|
export { type GithubComment, GithubConfig, Retry, apply, commentTrigger, githubKind, inject, name, pollFailure, pollRound, pollThreads, pollTicketPulls };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as reached, A as LOCAL_TARGET, B as ThreadRef, C as ChannelRef, D as Execution, E as Evaluation, F as Strength, G as Trigger, H as TicketEndBy, I as Text, J as followupOwners, K as TriggerDisposition, L as Thread, M as MandateBasis, N as PolicyEffect, O as Follow, P as ReportState, Q as liveTicket, R as ThreadFact, S as Attention, T as DecisionKind, U as TicketInput, V as Ticket, W as TicketOutcome, X as latestAssignment, Y as isoTime, Z as latestReport, _ as ActorRef, a as DomainEvent, at as ticketOutcome, b as AssignmentState, c as GateResult, ct as Reason, d as Policy, dt as said, et as readUntil, f as ReplyStatus, ft as Config, g as AcceptMode, h as SourceScope, i as Candidate, it as threadKey, j as Mandate, k as FollowupRecords, l as OutboundMessage, lt as ReasonCode, m as ScopeResult, mt as DeploymentConfig, n as AssessmentOutcome, nt as settled, o as DomainEvents, ot as waitsForPerson, p as Repositories, pt as Deployment, q as assignmentTargetOf, r as Assessor, rt as targetOf, s as Executor, st as REASONS, t as ReceiveResult, tt as runningAssignment, u as Outbox, ut as reasonText, v as AdmissionCell, w as Decision, x as AssignmentTarget, y as Assignment, z as ThreadFacts } from "./intake-
|
|
1
|
+
import { $ as reached, A as LOCAL_TARGET, B as ThreadRef, C as ChannelRef, D as Execution, E as Evaluation, F as Strength, G as Trigger, H as TicketEndBy, I as Text, J as followupOwners, K as TriggerDisposition, L as Thread, M as MandateBasis, N as PolicyEffect, O as Follow, P as ReportState, Q as liveTicket, R as ThreadFact, S as Attention, T as DecisionKind, U as TicketInput, V as Ticket, W as TicketOutcome, X as latestAssignment, Y as isoTime, Z as latestReport, _ as ActorRef, a as DomainEvent, at as ticketOutcome, b as AssignmentState, c as GateResult, ct as Reason, d as Policy, dt as said, et as readUntil, f as ReplyStatus, ft as Config, g as AcceptMode, h as SourceScope, i as Candidate, it as threadKey, j as Mandate, k as FollowupRecords, l as OutboundMessage, lt as ReasonCode, m as ScopeResult, mt as DeploymentConfig, n as AssessmentOutcome, nt as settled, o as DomainEvents, ot as waitsForPerson, p as Repositories, pt as Deployment, q as assignmentTargetOf, r as Assessor, rt as targetOf, s as Executor, st as REASONS, t as ReceiveResult, tt as runningAssignment, u as Outbox, ut as reasonText, v as AdmissionCell, w as Decision, x as AssignmentTarget, y as Assignment, z as ThreadFacts } from "./intake-B1UsoHuw.js";
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
//#region src/core/part.d.ts
|
|
4
4
|
interface EngineOptions {
|
package/lib/index.js
CHANGED
|
@@ -5491,6 +5491,13 @@ var DshExecutor = class {
|
|
|
5491
5491
|
const ticket = this.host.store.tickets.get(ticketId);
|
|
5492
5492
|
const assignment = ticket && latestAssignment(ticket);
|
|
5493
5493
|
if (!ticket || !liveTicket(ticket) || assignment?.id !== assignmentId || assignment.state !== "pending") return;
|
|
5494
|
+
if (ticket.blockedReason !== void 0) {
|
|
5495
|
+
this.host.engine.report(ticketId, {
|
|
5496
|
+
state: "blocked",
|
|
5497
|
+
summary: ticket.blockedReason
|
|
5498
|
+
});
|
|
5499
|
+
return;
|
|
5500
|
+
}
|
|
5494
5501
|
this.slots.reserve(ticketId, "launch");
|
|
5495
5502
|
try {
|
|
5496
5503
|
await this.launch(ticket, assignment);
|
|
@@ -8802,6 +8809,10 @@ var RouteError = class extends Error {
|
|
|
8802
8809
|
this.status = status;
|
|
8803
8810
|
}
|
|
8804
8811
|
};
|
|
8812
|
+
/** Whether a registration failed because the id is taken, the only failure that is the caller's to fix. */
|
|
8813
|
+
function isDuplicateDevice(error) {
|
|
8814
|
+
return error instanceof Error && /is already registered/.test(error.message);
|
|
8815
|
+
}
|
|
8805
8816
|
async function readBody(req, maxBytes = 131072) {
|
|
8806
8817
|
let size = 0;
|
|
8807
8818
|
const chunks = [];
|
|
@@ -8924,12 +8935,13 @@ function failure(error, internal) {
|
|
|
8924
8935
|
id
|
|
8925
8936
|
}];
|
|
8926
8937
|
}
|
|
8927
|
-
function machineView({ stateDir, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs }) {
|
|
8938
|
+
function machineView({ stateDir, promptMaxChars, admissionContextMaxPages, admissionContextTimeoutMs, hubPort }) {
|
|
8928
8939
|
return {
|
|
8929
8940
|
stateDir,
|
|
8930
8941
|
promptMaxChars: promptMaxChars.get(),
|
|
8931
8942
|
admissionContextMaxPages: admissionContextMaxPages.get(),
|
|
8932
|
-
admissionContextTimeoutMs: admissionContextTimeoutMs.get()
|
|
8943
|
+
admissionContextTimeoutMs: admissionContextTimeoutMs.get(),
|
|
8944
|
+
hubPort
|
|
8933
8945
|
};
|
|
8934
8946
|
}
|
|
8935
8947
|
/** Every setup problem the panel shows: global first, then the ones that pause a single Source, each with that Source's title. */
|
|
@@ -9169,11 +9181,51 @@ function routes(runtime) {
|
|
|
9169
9181
|
devices: runtime.devices(),
|
|
9170
9182
|
online: runtime.onlineDevices()
|
|
9171
9183
|
}),
|
|
9184
|
+
/** 登记一台新设备。令牌只在这一次响应里出现,刷新就没了。 */
|
|
9185
|
+
"POST devices": async (input) => {
|
|
9186
|
+
const body = z.object({
|
|
9187
|
+
id: z.string().trim().min(1),
|
|
9188
|
+
name: z.string().trim().min(1),
|
|
9189
|
+
labels: z.array(z.string().trim().min(1)).optional(),
|
|
9190
|
+
roots: z.array(z.object({
|
|
9191
|
+
project: z.string().trim().min(1),
|
|
9192
|
+
path: z.string().trim().min(1)
|
|
9193
|
+
}))
|
|
9194
|
+
}).parse(await input.body());
|
|
9195
|
+
try {
|
|
9196
|
+
return runtime.addDevice(body);
|
|
9197
|
+
} catch (error) {
|
|
9198
|
+
if (isDuplicateDevice(error)) throw new RouteError(400, error.message);
|
|
9199
|
+
throw error;
|
|
9200
|
+
}
|
|
9201
|
+
},
|
|
9172
9202
|
/** 吊销一台设备:登记保留(审计要看),下一次心跳就断开。 */
|
|
9173
9203
|
"POST devices/:id/revoke": ({ params }) => {
|
|
9174
9204
|
runtime.revokeDevice(decodeURIComponent(params.id));
|
|
9175
9205
|
return { revoked: true };
|
|
9176
9206
|
},
|
|
9207
|
+
/** 删掉一台设备。和吊销不同:吊销留着记录,删除连记录一起去掉。 */
|
|
9208
|
+
"DELETE devices/:id": ({ params }) => {
|
|
9209
|
+
runtime.deleteDevice(decodeURIComponent(params.id));
|
|
9210
|
+
return { deleted: true };
|
|
9211
|
+
},
|
|
9212
|
+
/** 改一台设备的名字、标签或项目;id 与令牌不动,所以那台设备不用重新配置。 */
|
|
9213
|
+
"PUT devices/:id": async ({ params, body: readBody }) => {
|
|
9214
|
+
const body = z.object({
|
|
9215
|
+
name: z.string().trim().min(1).optional(),
|
|
9216
|
+
labels: z.array(z.string().trim().min(1)).optional(),
|
|
9217
|
+
roots: z.array(z.object({
|
|
9218
|
+
project: z.string().trim().min(1),
|
|
9219
|
+
path: z.string().trim().min(1)
|
|
9220
|
+
})).optional()
|
|
9221
|
+
}).parse(await readBody());
|
|
9222
|
+
try {
|
|
9223
|
+
runtime.updateDevice(decodeURIComponent(params.id), body);
|
|
9224
|
+
} catch (error) {
|
|
9225
|
+
throw new RouteError(400, error instanceof Error ? error.message : String(error));
|
|
9226
|
+
}
|
|
9227
|
+
return { updated: true };
|
|
9228
|
+
},
|
|
9177
9229
|
/** 轮换令牌:旧令牌立刻失效,新令牌只返回这一次。 */
|
|
9178
9230
|
"POST devices/:id/rotate": ({ params }) => ({ token: runtime.rotateDeviceToken(decodeURIComponent(params.id)) }),
|
|
9179
9231
|
"GET prompts": ({ query }) => {
|
|
@@ -9481,6 +9533,36 @@ var Devices = class {
|
|
|
9481
9533
|
revokedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9482
9534
|
});
|
|
9483
9535
|
}
|
|
9536
|
+
/**
|
|
9537
|
+
* Delete a device outright, revoked or not.
|
|
9538
|
+
*
|
|
9539
|
+
* Revoking keeps the record for the audit trail; deleting is for the mistake — a device registered under the wrong
|
|
9540
|
+
* id, or one that will never come back — that an operator does not want cluttering the list forever. The two are
|
|
9541
|
+
* deliberately separate actions: revoke is the safe one, delete is the one that loses the history.
|
|
9542
|
+
*
|
|
9543
|
+
* @throws When the device is unknown, so a typo is reported rather than silently removing nothing.
|
|
9544
|
+
*/
|
|
9545
|
+
delete(id) {
|
|
9546
|
+
this.require(id);
|
|
9547
|
+
this.store.delete(KV.foreman.device(id));
|
|
9548
|
+
}
|
|
9549
|
+
/**
|
|
9550
|
+
* Replace what a device offers: its display name, its labels, and the projects it can work on.
|
|
9551
|
+
*
|
|
9552
|
+
* The id and the token are not touched: an id is what the device's own configuration already says, and rotating a
|
|
9553
|
+
* token is a separate decision with its own effect (the device must be given the new one). A handshake in flight
|
|
9554
|
+
* keeps using the record it read, which is why the hub drops the connection after a change.
|
|
9555
|
+
*
|
|
9556
|
+
* @throws When the device is unknown.
|
|
9557
|
+
*/
|
|
9558
|
+
update(id, change) {
|
|
9559
|
+
const next = { ...this.require(id) };
|
|
9560
|
+
if (change.name !== void 0) next.name = change.name;
|
|
9561
|
+
if (change.labels !== void 0) next.labels = [...change.labels];
|
|
9562
|
+
if (change.roots !== void 0) next.roots = change.roots.map((root) => ({ ...root }));
|
|
9563
|
+
this.store.set(KV.foreman.device(id), next);
|
|
9564
|
+
return next;
|
|
9565
|
+
}
|
|
9484
9566
|
/** Whether a token is this device's and the device is still allowed to connect; `false` for an unknown or revoked id. */
|
|
9485
9567
|
verify(id, token) {
|
|
9486
9568
|
const device = this.get(id);
|
|
@@ -9766,6 +9848,8 @@ var ForemanExecutor = class {
|
|
|
9766
9848
|
*/
|
|
9767
9849
|
var Hub = class {
|
|
9768
9850
|
connections = /* @__PURE__ */ new Map();
|
|
9851
|
+
/** Connections that have arrived but not yet verified: parked so they cannot disturb an established one. */
|
|
9852
|
+
pending = /* @__PURE__ */ new Map();
|
|
9769
9853
|
devices;
|
|
9770
9854
|
onFrame;
|
|
9771
9855
|
log;
|
|
@@ -9776,9 +9860,16 @@ var Hub = class {
|
|
|
9776
9860
|
this.log = input.log ?? (() => {});
|
|
9777
9861
|
this.now = input.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
9778
9862
|
}
|
|
9779
|
-
/**
|
|
9863
|
+
/**
|
|
9864
|
+
* Register a connection under the device id the transport says it belongs to; it is a claim until `hello`.
|
|
9865
|
+
*
|
|
9866
|
+
* A new connection does NOT evict an established one here. Evicting on arrival let two clients using the same id
|
|
9867
|
+
* close each other in a loop — each reconnect killed the other's socket, neither ever finished its handshake, and
|
|
9868
|
+
* the device stayed offline forever while both kept retrying. Instead a fresh connection waits in `pending` until
|
|
9869
|
+
* its `hello` verifies, and only then replaces whoever held the id (`admit`). A half-finished connection therefore
|
|
9870
|
+
* cannot disturb a working one, while a genuine reconnect still takes over once it has proved itself.
|
|
9871
|
+
*/
|
|
9780
9872
|
attach(deviceId, channel) {
|
|
9781
|
-
this.connections.get(deviceId)?.channel.close();
|
|
9782
9873
|
const connection = {
|
|
9783
9874
|
channel,
|
|
9784
9875
|
handshook: false,
|
|
@@ -9786,12 +9877,23 @@ var Hub = class {
|
|
|
9786
9877
|
lastSeenAt: this.now(),
|
|
9787
9878
|
dedupe: new FrameDedupe(/* @__PURE__ */ new Set())
|
|
9788
9879
|
};
|
|
9789
|
-
this.
|
|
9880
|
+
this.pending.set(channel, {
|
|
9881
|
+
deviceId,
|
|
9882
|
+
connection
|
|
9883
|
+
});
|
|
9790
9884
|
channel.onMessage((text) => this.receive(deviceId, connection, text));
|
|
9791
9885
|
channel.onClose(() => {
|
|
9886
|
+
this.pending.delete(channel);
|
|
9792
9887
|
if (this.connections.get(deviceId) === connection) this.connections.delete(deviceId);
|
|
9793
9888
|
});
|
|
9794
9889
|
}
|
|
9890
|
+
/** Promote a verified connection to be the one the id answers to, replacing whatever held it. */
|
|
9891
|
+
admit(deviceId, connection) {
|
|
9892
|
+
const previous = this.connections.get(deviceId);
|
|
9893
|
+
if (previous !== void 0 && previous !== connection) previous.channel.close();
|
|
9894
|
+
this.connections.set(deviceId, connection);
|
|
9895
|
+
this.pending.delete(connection.channel);
|
|
9896
|
+
}
|
|
9795
9897
|
/** Drop a device's connection and stop listing it as online. */
|
|
9796
9898
|
detach(deviceId) {
|
|
9797
9899
|
const connection = this.connections.get(deviceId);
|
|
@@ -9832,6 +9934,8 @@ var Hub = class {
|
|
|
9832
9934
|
}
|
|
9833
9935
|
/** Close every device connection, used at shutdown so in-flight work fails fast instead of hanging. */
|
|
9834
9936
|
closeAll() {
|
|
9937
|
+
for (const [, parked] of [...this.pending]) parked.connection.channel.close();
|
|
9938
|
+
this.pending.clear();
|
|
9835
9939
|
for (const [id, connection] of [...this.connections]) {
|
|
9836
9940
|
this.connections.delete(id);
|
|
9837
9941
|
connection.channel.close();
|
|
@@ -9892,6 +9996,7 @@ var Hub = class {
|
|
|
9892
9996
|
connection.running = [...frame.running];
|
|
9893
9997
|
connection.runningCount = connection.running.length;
|
|
9894
9998
|
connection.lastSeenAt = this.now();
|
|
9999
|
+
this.admit(deviceId, connection);
|
|
9895
10000
|
this.devices.seen(deviceId, connection.lastSeenAt);
|
|
9896
10001
|
this.onFrame(deviceId, frame);
|
|
9897
10002
|
}
|
|
@@ -9906,6 +10011,7 @@ var Hub = class {
|
|
|
9906
10011
|
}));
|
|
9907
10012
|
} catch {}
|
|
9908
10013
|
this.log(`foreman hub refused a device: ${reason}`);
|
|
10014
|
+
this.pending.delete(connection.channel);
|
|
9909
10015
|
connection.channel.close();
|
|
9910
10016
|
for (const [id, current] of this.connections) if (current === connection) this.connections.delete(id);
|
|
9911
10017
|
}
|
|
@@ -10157,6 +10263,14 @@ function prepareTicketWithForeman(input) {
|
|
|
10157
10263
|
const labels = input.labelsOf?.(trigger);
|
|
10158
10264
|
if (labels !== void 0 && labels.length > 0) request.labels = labels;
|
|
10159
10265
|
const target = select(request, input.devices(), input.online());
|
|
10266
|
+
const mustBeRemote = request.device !== void 0 || request.labels !== void 0 && request.labels.length > 0;
|
|
10267
|
+
if (target === void 0 && mustBeRemote) {
|
|
10268
|
+
const blockedReason = `no online device can take this: it asked for ${request.device !== void 0 ? `device "${request.device}"` : "a restricted machine"} on project "${request.project}"; nothing was run`;
|
|
10269
|
+
return {
|
|
10270
|
+
...ticket,
|
|
10271
|
+
blockedReason
|
|
10272
|
+
};
|
|
10273
|
+
}
|
|
10160
10274
|
const prepared = input.local({
|
|
10161
10275
|
...ticket,
|
|
10162
10276
|
...target === void 0 ? {} : { target }
|
|
@@ -12376,6 +12490,14 @@ var Runtime = class extends Service {
|
|
|
12376
12490
|
models: () => this.ctx.sessionController.modelCatalog(),
|
|
12377
12491
|
devices: () => runtime.devices.list(),
|
|
12378
12492
|
onlineDevices: () => runtime.hub?.snapshot() ?? [],
|
|
12493
|
+
/** 登记一台设备并铸一枚令牌;和 `scripts/add-device.mjs` 走同一个 `Devices.add`。 */
|
|
12494
|
+
addDevice: (input) => {
|
|
12495
|
+
const { device, token } = runtime.devices.add(input);
|
|
12496
|
+
return {
|
|
12497
|
+
id: device.id,
|
|
12498
|
+
token
|
|
12499
|
+
};
|
|
12500
|
+
},
|
|
12379
12501
|
revokeDevice: (id) => {
|
|
12380
12502
|
runtime.devices.revoke(id);
|
|
12381
12503
|
runtime.hub?.detach(id);
|
|
@@ -12384,6 +12506,14 @@ var Runtime = class extends Service {
|
|
|
12384
12506
|
const token = runtime.devices.rotate(id);
|
|
12385
12507
|
runtime.hub?.detach(id);
|
|
12386
12508
|
return token;
|
|
12509
|
+
},
|
|
12510
|
+
deleteDevice: (id) => {
|
|
12511
|
+
runtime.hub?.detach(id);
|
|
12512
|
+
runtime.devices.delete(id);
|
|
12513
|
+
},
|
|
12514
|
+
updateDevice: (id, change) => {
|
|
12515
|
+
runtime.devices.update(id, change);
|
|
12516
|
+
runtime.hub?.detach(id);
|
|
12387
12517
|
}
|
|
12388
12518
|
};
|
|
12389
12519
|
}
|
|
@@ -485,6 +485,14 @@ interface Ticket {
|
|
|
485
485
|
* It lives on the Ticket because `newAssignment` never sees the accept outcome that made the choice.
|
|
486
486
|
*/
|
|
487
487
|
target?: AssignmentTarget;
|
|
488
|
+
/**
|
|
489
|
+
* Why this ticket cannot start, when its acceptance required a machine and none could take it.
|
|
490
|
+
*
|
|
491
|
+
* Set instead of throwing: the request is real and worth recording, but running it on the host would answer a
|
|
492
|
+
* different question than the one asked. The executor reports it `blocked` without starting a Session, so it
|
|
493
|
+
* reaches a person as a visible failure rather than a silent success somewhere else.
|
|
494
|
+
*/
|
|
495
|
+
blockedReason?: string;
|
|
488
496
|
inputs: TicketInput[];
|
|
489
497
|
/** Inputs not yet handed to a running assignment. */
|
|
490
498
|
undelivered: string[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as ThreadRef, C as ChannelRef, G as Trigger, L as Thread, R as ThreadFact, V as Ticket, h as SourceScope, l as OutboundMessage, m as ScopeResult, pt as Deployment, t as ReceiveResult, v as AdmissionCell } from "./intake-
|
|
1
|
+
import { B as ThreadRef, C as ChannelRef, G as Trigger, L as Thread, R as ThreadFact, V as Ticket, h as SourceScope, l as OutboundMessage, m as ScopeResult, pt as Deployment, t as ReceiveResult, v as AdmissionCell } from "./intake-B1UsoHuw.js";
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
package/lib/local-watch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { G as Trigger } from "./intake-
|
|
2
|
-
import { t as SourceKind } from "./kind-
|
|
1
|
+
import { G as Trigger } from "./intake-B1UsoHuw.js";
|
|
2
|
+
import { t as SourceKind } from "./kind-BYtP14oM.js";
|
|
3
3
|
import { Context } from "@deepseek-ai/cordis";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
//#region src/sources/dsh.d.ts
|
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.1",
|
|
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
|