@linxin666/dsh-client-ui-task-board 0.2.5 → 0.2.7
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/README.i18n.yaml +2 -2
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +1 -1
- package/lib/index.js +84 -6
- package/package.json +14 -14
- package/src/client/settings-card.module.css +2 -0
- package/src/host-ledger.ts +73 -4
- package/src/index.ts +2 -2
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# node scripts/verify-docs.mjs --write <dir>
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: d65b9b3213fd504715bffb3a00be17074703e8c6
|
|
6
|
+
README.zh.md: 09705ff5ff6cfdc3df7896cd0af6489c35fbe174
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-task-board
|
|
|
52
52
|
| Key | Default | Behavior |
|
|
53
53
|
| --- | --- | --- |
|
|
54
54
|
| `enabled` | `true` | Enables the Host service and browser board. |
|
|
55
|
-
| `announceToAgent` | `
|
|
55
|
+
| `announceToAgent` | `false` | Opt-in: when true, adds the task-board guidance section to agent system prompts. |
|
|
56
56
|
| `preventIdleSleep` | `false` | Holds one system idle-sleep assertion while any DSH session runs, any schedule is enabled, or session state is unknown. |
|
|
57
57
|
| `trustedProxyHosts` | `[]` | Canonical `host[:port]` authorities accepted only through the authenticated loopback reverse-proxy path. |
|
|
58
58
|
| `proxyTokenEnv` | `DSH_TASK_BOARD_PROXY_TOKEN` | Environment variable containing the reverse-proxy token; the token itself is never stored in plugin config. |
|
package/README.zh.md
CHANGED
|
@@ -52,7 +52,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-task-board
|
|
|
52
52
|
| 键 | 默认值 | 行为 |
|
|
53
53
|
| --- | --- | --- |
|
|
54
54
|
| `enabled` | `true` | 启用 Host 服务与浏览器看板。 |
|
|
55
|
-
| `announceToAgent` | `
|
|
55
|
+
| `announceToAgent` | `false` | 按需开启:开启后向 agent 系统提示加入任务看板说明。 |
|
|
56
56
|
| `preventIdleSleep` | `false` | 存在运行中的 DSH 会话、已启用计划或未知会话状态时,持有一个系统空闲睡眠断言。 |
|
|
57
57
|
| `trustedProxyHosts` | `[]` | 仅通过已认证 loopback 反向代理路径接受的规范 `host[:port]` authority 白名单。 |
|
|
58
58
|
| `proxyTokenEnv` | `DSH_TASK_BOARD_PROXY_TOKEN` | 保存反向代理 token 的环境变量名;token 本身不会写入插件配置。 |
|
package/lib/client.js
CHANGED
|
@@ -2908,7 +2908,7 @@ window.__ModuleLoader__.load({
|
|
|
2908
2908
|
}
|
|
2909
2909
|
//#endregion
|
|
2910
2910
|
//#region \0dsh-css:packages/dsh-task-board/src/client/settings-card.module.css.mjs
|
|
2911
|
-
const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Jh0q7G_selectWrap{position:relative}.Jh0q7G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Jh0q7G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Jh0q7G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_selectChevronOpen{transform:rotate(180deg)}.Jh0q7G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Jh0q7G_selectPopupOpen{opacity:1;transform:none}.Jh0q7G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Jh0q7G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Jh0q7G_selectOption:hover,.Jh0q7G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Jh0q7G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Jh0q7G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save,.Jh0q7G_selectChevron,.Jh0q7G_selectChevronOpen,.Jh0q7G_selectPopup{transition:none}}";
|
|
2911
|
+
const css = ".Jh0q7G_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.Jh0q7G_card:hover{border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.Jh0q7G_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Jh0q7G_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.Jh0q7G_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.Jh0q7G_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.Jh0q7G_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.Jh0q7G_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_chevronOpen{transform:rotate(180deg)}.Jh0q7G_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.Jh0q7G_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.Jh0q7G_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.Jh0q7G_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.Jh0q7G_discard,.Jh0q7G_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.Jh0q7G_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.Jh0q7G_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.Jh0q7G_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.Jh0q7G_discard:disabled,.Jh0q7G_save:disabled{opacity:.4;cursor:default}.Jh0q7G_discard:focus-visible,.Jh0q7G_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.Jh0q7G_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.Jh0q7G_field+.Jh0q7G_field{border-top:1px solid var(--dsw-alias-border-l2)}.Jh0q7G_head{align-items:center;gap:8px;display:flex}.Jh0q7G_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.Jh0q7G_badges{align-items:center;gap:8px;display:inline-flex}.Jh0q7G_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.Jh0q7G_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.Jh0q7G_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.Jh0q7G_reset:disabled{cursor:default}.Jh0q7G_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.Jh0q7G_input,.Jh0q7G_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_input:focus-visible,.Jh0q7G_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.Jh0q7G_input:disabled,.Jh0q7G_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.Jh0q7G_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.Jh0q7G_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.Jh0q7G_selectWrap{position:relative}.Jh0q7G_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.Jh0q7G_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Jh0q7G_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.Jh0q7G_selectChevronOpen{transform:rotate(180deg)}.Jh0q7G_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.Jh0q7G_selectPopupOpen{opacity:1;transform:none}.Jh0q7G_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.Jh0q7G_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;flex-shrink:0;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.Jh0q7G_selectOption:hover,.Jh0q7G_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.Jh0q7G_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.Jh0q7G_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.Jh0q7G_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.Jh0q7G_card,.Jh0q7G_header,.Jh0q7G_chevron,.Jh0q7G_chevronOpen,.Jh0q7G_discard,.Jh0q7G_save,.Jh0q7G_selectChevron,.Jh0q7G_selectChevronOpen,.Jh0q7G_selectPopup{transition:none}}";
|
|
2912
2912
|
const tagId = "@linxin666/dsh-client-ui-task-board/settings-card.module.css";
|
|
2913
2913
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
2914
2914
|
const tag = document.createElement("style");
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
2
|
import z from "schemastery";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
4
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
4
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { dirname, isAbsolute, join, win32 } from "node:path";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
8
8
|
//#region src/core/schedule.ts
|
|
9
9
|
/** Inclusive ranges per field, in cron order. */
|
|
10
10
|
const FIELD_RANGES = [
|
|
@@ -861,6 +861,69 @@ function processIsAlive(pid) {
|
|
|
861
861
|
return error.code !== "ESRCH";
|
|
862
862
|
}
|
|
863
863
|
}
|
|
864
|
+
const PROCESS_PROBE_TIMEOUT_MS = 3e3;
|
|
865
|
+
let ownStartTime;
|
|
866
|
+
let ownStartTimeResolved = false;
|
|
867
|
+
/**
|
|
868
|
+
* Best-effort start time (Unix epoch ms) of a live process. Used to prove
|
|
869
|
+
* whether the ledger lock really belongs to the PID recorded in it, so a
|
|
870
|
+
* crash leftover whose PID was reused by an unrelated process (issue #786)
|
|
871
|
+
* is detected as stale instead of blocking startup forever. Returns
|
|
872
|
+
* undefined when the platform probe is unavailable; callers fail closed.
|
|
873
|
+
*/
|
|
874
|
+
function processStartTimeMs(pid) {
|
|
875
|
+
if (process.platform === "win32") {
|
|
876
|
+
const probe = spawnSync("powershell", [
|
|
877
|
+
"-NoProfile",
|
|
878
|
+
"-NonInteractive",
|
|
879
|
+
"-Command",
|
|
880
|
+
"[DateTimeOffset]::FromFileTime((Get-Process -Id " + String(pid) + " -ErrorAction SilentlyContinue).StartTime.ToUniversalTime().ToFileTime()).ToUnixTimeMilliseconds()"
|
|
881
|
+
], {
|
|
882
|
+
timeout: PROCESS_PROBE_TIMEOUT_MS,
|
|
883
|
+
windowsHide: true
|
|
884
|
+
});
|
|
885
|
+
if (probe.status !== 0 || probe.stdout.length === 0) return void 0;
|
|
886
|
+
const started = Number(probe.stdout.toString("utf8").trim());
|
|
887
|
+
return Number.isFinite(started) ? started : void 0;
|
|
888
|
+
}
|
|
889
|
+
const env = {
|
|
890
|
+
...process.env,
|
|
891
|
+
LC_ALL: "C"
|
|
892
|
+
};
|
|
893
|
+
const probe = spawnSync("ps", [
|
|
894
|
+
"-o",
|
|
895
|
+
"lstart=",
|
|
896
|
+
"-p",
|
|
897
|
+
String(pid)
|
|
898
|
+
], {
|
|
899
|
+
timeout: PROCESS_PROBE_TIMEOUT_MS,
|
|
900
|
+
env
|
|
901
|
+
});
|
|
902
|
+
if (probe.status === 0 && probe.stdout.length > 0) {
|
|
903
|
+
const started = Date.parse(probe.stdout.toString("utf8").trim());
|
|
904
|
+
if (Number.isFinite(started)) return started;
|
|
905
|
+
}
|
|
906
|
+
const elapsed = spawnSync("ps", [
|
|
907
|
+
"-o",
|
|
908
|
+
"etimes=",
|
|
909
|
+
"-p",
|
|
910
|
+
String(pid)
|
|
911
|
+
], {
|
|
912
|
+
timeout: PROCESS_PROBE_TIMEOUT_MS,
|
|
913
|
+
env
|
|
914
|
+
});
|
|
915
|
+
if (elapsed.status !== 0 || elapsed.stdout.length === 0) return void 0;
|
|
916
|
+
const seconds = Number(elapsed.stdout.toString("utf8").trim());
|
|
917
|
+
if (!Number.isFinite(seconds)) return void 0;
|
|
918
|
+
return Date.now() - seconds * 1e3;
|
|
919
|
+
}
|
|
920
|
+
function ownProcessStartTimeMs() {
|
|
921
|
+
if (!ownStartTimeResolved) {
|
|
922
|
+
ownStartTimeResolved = true;
|
|
923
|
+
ownStartTime = processStartTimeMs(process.pid);
|
|
924
|
+
}
|
|
925
|
+
return ownStartTime;
|
|
926
|
+
}
|
|
864
927
|
function betterExecution(a, b) {
|
|
865
928
|
if (a.endedAt === void 0 && b.endedAt !== void 0) return b;
|
|
866
929
|
if (b.endedAt === void 0 && a.endedAt !== void 0) return a;
|
|
@@ -1308,7 +1371,8 @@ var HostTaskLedger = class {
|
|
|
1308
1371
|
const fd = openSync(this.lockFile, "wx", 384);
|
|
1309
1372
|
writeFileSync(fd, JSON.stringify({
|
|
1310
1373
|
pid: process.pid,
|
|
1311
|
-
token: this.lockToken
|
|
1374
|
+
token: this.lockToken,
|
|
1375
|
+
startedAt: ownProcessStartTimeMs()
|
|
1312
1376
|
}), { encoding: "utf8" });
|
|
1313
1377
|
fsyncSync(fd);
|
|
1314
1378
|
try {
|
|
@@ -1318,13 +1382,27 @@ var HostTaskLedger = class {
|
|
|
1318
1382
|
} catch (error) {
|
|
1319
1383
|
if (error.code !== "EEXIST") throw error;
|
|
1320
1384
|
let pid;
|
|
1385
|
+
let ownerStartedAt;
|
|
1321
1386
|
try {
|
|
1322
1387
|
const owner = JSON.parse(readFileSync(this.lockFile, "utf8"));
|
|
1323
1388
|
if (typeof owner.pid === "number") pid = owner.pid;
|
|
1389
|
+
if (typeof owner.startedAt === "number") ownerStartedAt = owner.startedAt;
|
|
1324
1390
|
} catch {
|
|
1325
1391
|
throw new Error(`task-board ledger lock is unreadable: ${this.lockFile}`);
|
|
1326
1392
|
}
|
|
1327
|
-
if (pid !== void 0 && processIsAlive(pid))
|
|
1393
|
+
if (pid !== void 0 && processIsAlive(pid)) {
|
|
1394
|
+
const actualStartedAt = pid === process.pid ? ownProcessStartTimeMs() : processStartTimeMs(pid);
|
|
1395
|
+
if (!(actualStartedAt !== void 0 && (ownerStartedAt !== void 0 ? actualStartedAt !== ownerStartedAt : (() => {
|
|
1396
|
+
try {
|
|
1397
|
+
return statSync(this.lockFile).mtimeMs < actualStartedAt;
|
|
1398
|
+
} catch {
|
|
1399
|
+
return true;
|
|
1400
|
+
}
|
|
1401
|
+
})()))) {
|
|
1402
|
+
const hint = ownerStartedAt !== void 0 && actualStartedAt === ownerStartedAt ? "" : `; if this PID was reused after a crash and no other DSH host is running, remove ${this.lockFile} manually and retry`;
|
|
1403
|
+
throw new Error(`task-board ledger is already owned by process ${pid}${hint}`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1328
1406
|
try {
|
|
1329
1407
|
unlinkSync(this.lockFile);
|
|
1330
1408
|
} catch (unlinkError) {
|
|
@@ -2286,7 +2364,7 @@ const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI
|
|
|
2286
2364
|
*/
|
|
2287
2365
|
const TASK_BOARD_SETTINGS_NAMESPACE = settingsNamespace("task-board");
|
|
2288
2366
|
const Config = z.object({
|
|
2289
|
-
announceToAgent: z.boolean().default(
|
|
2367
|
+
announceToAgent: z.boolean().default(false),
|
|
2290
2368
|
enabled: z.boolean().default(true),
|
|
2291
2369
|
preventIdleSleep: z.boolean().default(false),
|
|
2292
2370
|
trustedProxyHosts: z.array(z.string()).default([]),
|
|
@@ -2306,7 +2384,7 @@ function resolveProxyAccess(config, env = process.env) {
|
|
|
2306
2384
|
};
|
|
2307
2385
|
}
|
|
2308
2386
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
2309
|
-
const DEFAULT_ANNOUNCE =
|
|
2387
|
+
const DEFAULT_ANNOUNCE = false;
|
|
2310
2388
|
/**
|
|
2311
2389
|
* Register the board's announcement section, gated on the composition entry's
|
|
2312
2390
|
* `announceToAgent` (and the live settings value once the web settings
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
39
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
38
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
|
|
39
|
+
"@deepseek-ai/dsh-commands": "^0.1.1-rc.1",
|
|
40
40
|
"react": "^18.2.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
@@ -44,17 +44,17 @@
|
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
47
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
48
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
49
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
50
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
53
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
54
|
-
"@deepseek-ai/dsh-host-apiproxy": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
47
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
|
|
49
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
|
|
50
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.1-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-commands": "^0.1.1-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.1",
|
|
58
58
|
"@types/react": "~18.3.1",
|
|
59
59
|
"@types/react-dom": "^18.3.5",
|
|
60
60
|
"@types/node": "^22.20.0",
|
package/src/host-ledger.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
1
2
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
4
|
import { dirname, join } from 'node:path'
|
|
4
5
|
import { dshHome } from './dsh-home.ts'
|
|
5
6
|
import { isValidCron, nextRunAtMs } from './core/schedule.ts'
|
|
@@ -68,6 +69,53 @@ function processIsAlive(pid: number): boolean {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
const PROCESS_PROBE_TIMEOUT_MS = 3000
|
|
73
|
+
|
|
74
|
+
let ownStartTime: number | undefined
|
|
75
|
+
let ownStartTimeResolved = false
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Best-effort start time (Unix epoch ms) of a live process. Used to prove
|
|
79
|
+
* whether the ledger lock really belongs to the PID recorded in it, so a
|
|
80
|
+
* crash leftover whose PID was reused by an unrelated process (issue #786)
|
|
81
|
+
* is detected as stale instead of blocking startup forever. Returns
|
|
82
|
+
* undefined when the platform probe is unavailable; callers fail closed.
|
|
83
|
+
*/
|
|
84
|
+
function processStartTimeMs(pid: number): number | undefined {
|
|
85
|
+
if (process.platform === 'win32') {
|
|
86
|
+
const probe = spawnSync(
|
|
87
|
+
'powershell',
|
|
88
|
+
['-NoProfile', '-NonInteractive', '-Command',
|
|
89
|
+
'[DateTimeOffset]::FromFileTime((Get-Process -Id ' + String(pid) + ' -ErrorAction SilentlyContinue).StartTime.ToUniversalTime().ToFileTime()).ToUnixTimeMilliseconds()'],
|
|
90
|
+
{ timeout: PROCESS_PROBE_TIMEOUT_MS, windowsHide: true },
|
|
91
|
+
)
|
|
92
|
+
if (probe.status !== 0 || probe.stdout.length === 0) return undefined
|
|
93
|
+
const started = Number(probe.stdout.toString('utf8').trim())
|
|
94
|
+
return Number.isFinite(started) ? started : undefined
|
|
95
|
+
}
|
|
96
|
+
// POSIX: ps lstart with a forced English locale, falling back to the
|
|
97
|
+
// elapsed-seconds column when lstart cannot be parsed.
|
|
98
|
+
const env = { ...process.env, LC_ALL: 'C' }
|
|
99
|
+
const probe = spawnSync('ps', ['-o', 'lstart=', '-p', String(pid)], { timeout: PROCESS_PROBE_TIMEOUT_MS, env })
|
|
100
|
+
if (probe.status === 0 && probe.stdout.length > 0) {
|
|
101
|
+
const started = Date.parse(probe.stdout.toString('utf8').trim())
|
|
102
|
+
if (Number.isFinite(started)) return started
|
|
103
|
+
}
|
|
104
|
+
const elapsed = spawnSync('ps', ['-o', 'etimes=', '-p', String(pid)], { timeout: PROCESS_PROBE_TIMEOUT_MS, env })
|
|
105
|
+
if (elapsed.status !== 0 || elapsed.stdout.length === 0) return undefined
|
|
106
|
+
const seconds = Number(elapsed.stdout.toString('utf8').trim())
|
|
107
|
+
if (!Number.isFinite(seconds)) return undefined
|
|
108
|
+
return Date.now() - seconds * 1000
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ownProcessStartTimeMs(): number | undefined {
|
|
112
|
+
if (!ownStartTimeResolved) {
|
|
113
|
+
ownStartTimeResolved = true
|
|
114
|
+
ownStartTime = processStartTimeMs(process.pid)
|
|
115
|
+
}
|
|
116
|
+
return ownStartTime
|
|
117
|
+
}
|
|
118
|
+
|
|
71
119
|
function betterExecution(a: ExecutionRecord, b: ExecutionRecord): ExecutionRecord {
|
|
72
120
|
if (a.endedAt === undefined && b.endedAt !== undefined) return b
|
|
73
121
|
if (b.endedAt === undefined && a.endedAt !== undefined) return a
|
|
@@ -512,7 +560,7 @@ export class HostTaskLedger {
|
|
|
512
560
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
513
561
|
try {
|
|
514
562
|
const fd = openSync(this.lockFile, 'wx', 0o600)
|
|
515
|
-
writeFileSync(fd, JSON.stringify({ pid: process.pid, token: this.lockToken }), { encoding: 'utf8' })
|
|
563
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, token: this.lockToken, startedAt: ownProcessStartTimeMs() }), { encoding: 'utf8' })
|
|
516
564
|
fsyncSync(fd)
|
|
517
565
|
try { chmodSync(this.lockFile, 0o600) } catch { /* Windows ACLs own access */ }
|
|
518
566
|
return fd
|
|
@@ -520,14 +568,35 @@ export class HostTaskLedger {
|
|
|
520
568
|
const code = (error as NodeJS.ErrnoException).code
|
|
521
569
|
if (code !== 'EEXIST') throw error
|
|
522
570
|
let pid: number | undefined
|
|
571
|
+
let ownerStartedAt: number | undefined
|
|
523
572
|
try {
|
|
524
|
-
const owner = JSON.parse(readFileSync(this.lockFile, 'utf8')) as { pid?: unknown }
|
|
573
|
+
const owner = JSON.parse(readFileSync(this.lockFile, 'utf8')) as { pid?: unknown; startedAt?: unknown }
|
|
525
574
|
if (typeof owner.pid === 'number') pid = owner.pid
|
|
575
|
+
if (typeof owner.startedAt === 'number') ownerStartedAt = owner.startedAt
|
|
526
576
|
} catch {
|
|
527
577
|
throw new Error(`task-board ledger lock is unreadable: ${this.lockFile}`)
|
|
528
578
|
}
|
|
529
579
|
if (pid !== undefined && processIsAlive(pid)) {
|
|
530
|
-
|
|
580
|
+
const actualStartedAt = pid === process.pid ? ownProcessStartTimeMs() : processStartTimeMs(pid)
|
|
581
|
+
// A reused PID is exposed when the live process identity no longer
|
|
582
|
+
// matches the recorded one: either the recorded start time differs,
|
|
583
|
+
// or (legacy locks without one) the lock file predates the live
|
|
584
|
+
// process and therefore cannot have been written by it. Takeover is
|
|
585
|
+
// safe in both cases — the original owner is gone.
|
|
586
|
+
const staleReuse = actualStartedAt !== undefined && (
|
|
587
|
+
ownerStartedAt !== undefined
|
|
588
|
+
? actualStartedAt !== ownerStartedAt
|
|
589
|
+
: (() => {
|
|
590
|
+
try { return statSync(this.lockFile).mtimeMs < actualStartedAt } catch { return true }
|
|
591
|
+
})()
|
|
592
|
+
)
|
|
593
|
+
if (!staleReuse) {
|
|
594
|
+
const confirmedOwner = ownerStartedAt !== undefined && actualStartedAt === ownerStartedAt
|
|
595
|
+
const hint = confirmedOwner
|
|
596
|
+
? ''
|
|
597
|
+
: `; if this PID was reused after a crash and no other DSH host is running, remove ${this.lockFile} manually and retry`
|
|
598
|
+
throw new Error(`task-board ledger is already owned by process ${pid}${hint}`)
|
|
599
|
+
}
|
|
531
600
|
}
|
|
532
601
|
try { unlinkSync(this.lockFile) } catch (unlinkError) {
|
|
533
602
|
if ((unlinkError as NodeJS.ErrnoException).code !== 'ENOENT') throw unlinkError
|
package/src/index.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface Config {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export const Config: z<Config> = z.object({
|
|
58
|
-
announceToAgent: z.boolean().default(
|
|
58
|
+
announceToAgent: z.boolean().default(false),
|
|
59
59
|
enabled: z.boolean().default(true),
|
|
60
60
|
preventIdleSleep: z.boolean().default(false),
|
|
61
61
|
trustedProxyHosts: z.array(z.string()).default([]),
|
|
@@ -76,7 +76,7 @@ export function resolveProxyAccess(config: Config | undefined, env: NodeJS.Proce
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
79
|
-
const DEFAULT_ANNOUNCE =
|
|
79
|
+
const DEFAULT_ANNOUNCE = false
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Register the board's announcement section, gated on the composition entry's
|