概览
@gausszhou/dsh-where-am-i
README / ZH
插件文档
dsh-where-am-i
Neofetch for DeepSeek Harness.
English | 简体中文
DeepSeek Harness (dsh) 插件:把当前设备信息以 neofetch 风格注入每个新建会话——OS 发行版、内核、架构、主机名、CPU、内存、uptime、时区、locale、shell/terminal、Node 版本。只在全新会话的第一步注入一次,之后持久保留在该会话历史里;已有会话完全不受影响。
一句话行为
- 全新会话(创建后第一次发送消息):注入一条设备信息 user 消息 → 模型在会话开头看到它;
- 已有会话(重启前就存在的、恢复的、延续中的):系统提示词与会话历史都不变,一条也不加;
- 同一个会话的后续步骤:永不重复注入(消息已持久化在历史里,直到 compaction 遮蔽);
- 子代理会话(subagent/工作流工具派生的临时会话):默认跳过(避免每次 fan-out 都重复);
includeSubagents: true可放开。
使用方法
将插件加进 dsh profile(发布为 npm 包后按包名安装):
dsh plugin --profile web add "@gausszhou/dsh-where-am-i"
本地开发时用 link 依赖(与 dsh-opencode-session-id 相同的方式):在 ~/.dsh/profiles/web/package.json 的 dependencies 加 "@gausszhou/dsh-where-am-i": "link:/home/gauss/Code/gausszhou/dsh-where-am-i",并把它加入 dsh.profile.bundles;插件自带的 cordis.patch.yml(dsh.bundle.patch)会完成 insert。
装完后重启 dsh web(systemctl --user restart dsh-web)让 bundle 生效。默认零配置即可用。
新会话里模型看到的内容(本机实测)
System info (the machine this dsh session runs on; sampled at session start):
host: gauss-KP
user: gauss
os: Ubuntu 26.04 LTS
kernel: 7.0.0-29-generic
arch: x86_64
cpu: 6 × Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
memory: 7.9 GiB used / 15.0 GiB total
uptime: 1d 4h 3m
timezone: Asia/Shanghai (GMT+08:00)
locale: zh_CN.UTF-8
shell: /bin/bash
term: dumb
node: v24.19.0
配置
默认零配置;全部可选项(均可省略,用默认值):
| 键 | 默认 | 说明 |
|---|---|---|
fields |
全部 13 项 | 只输出指定的字段行,按默认顺序排列,例如 [os, arch, timezone];未知字段名被忽略;过滤后为空则回退到全部 |
includeSubagents |
false |
true 时也会往子代理会话注入(默认跳过,避免 fan-out 重复) |
verbose |
false |
注入时把快照打到 stdout(web 部署可 journalctl -u dsh-web 看到) |
字段清单(emission 顺序):host user os kernel arch cpu memory uptime timezone locale shell term node。采样不到的字段(如无 SHELL 环境变量的 Windows)会自动省略该行。
# ~/.dsh/profiles/web/cordis.patch.yml
- id: where-am-i
config:
fields: [os, kernel, arch, cpu, memory, timezone]
设计说明:dsh 现有提示词里已有哪些信息
实现前对 dsh 现有提示词做了调研(dsh-system-prompt 的段注册与变量、dsh-agent-loop 的变量提供、各 runtime-context 包),已有内容如下,本插件只补缺口、不重复:
| 已有事实 | 来源 | 位置 |
|---|---|---|
You are an AI agent powered by DeepSeek Harness. |
dsh-system-prompt |
系统提示词段 harness:identity,order -100 |
| DSH 源码 checkout 路径 | dsh-app-boot / dsh-web-app |
段 harness:source,order -99 |
| Web GUI URL | dsh-web-app(仅 web profile) |
段 app:web-surface,order -98 |
| 部署 persona | 配置 | 段 deployment:persona,order 0 |
| 工具跨调用引导(read/write/edit/bash/web_search…) | 各 dsh-tool-* |
段 tool:*,order 100–199 |
{{provider}} {{model}} {{cwd}}(会话工作目录,每次组装求值) |
dsh-agent-loop |
提示词变量 |
| 文件沙箱策略、审批策略 | dsh-sandbox-policy / dsh-user-approval |
runtime context(user 角色快照) |
| 带时区时间戳 + 浏览器时区 + 经过时长 | dsh-time-context(默认组合未启用) |
runtime context |
| tmux session/window/pane 位置 | dsh-tmux-context(非 web 组合) |
runtime context |
AGENTS.md / CLAUDE.md 工作区指令 |
dsh-agent-instructions |
持久 user 消息 |
完全缺失(即本插件补的):OS/发行版名、内核 release、CPU 架构、主机名、CPU 型号与核心数、总/空闲内存、uptime、宿主 locale、shell/terminal、Node 版本、宿主进程时区。
刻意省略的字段
cwd:{{cwd}}变量已按会话实时提供,harness 也明确警告不要从 checkout 路径推断工作目录——不重复。- 日期/当前时间:冻结在历史里的墙钟会过期;实时时间戳由
dsh-time-context按请求提供(启用时)。 - ASCII logo / 桌面壁纸:纯 token 噪音,对模型无信息量。
为什么不是「系统提示词段」,而是「新会话一次性消息」
第一版实现用了 systemPrompt.section(),但那意味着:插件挂载后,每个会话的每一步都渲染同一段文本——既做不到「一次」,也会改写所有已有会话的系统提示词前缀(KV cache 从第一个变化的 token 起失效一次)。
现在的实现改为 dsh-time-context / dsh-tmux-context 同一条 seam:注册一个前置的 agent/pre-step waterfall 监听器,先 await next() 让下游先决策,仅当决策进入(enter)时,向该步批次追加一条持久、带来源的 user 消息。agent loop 会在 step/start 后把批次里的每条消息 session.append("user/message", …) 持久化,于是:
- 注入的消息成为该会话自己的历史(模型开头可见,compaction 前一直保留);
- 不注册任何系统提示词段 → 已有会话的系统提示词字节级不变,前缀与 KV cache 全部保留;
- 后续步骤(本会话或其他任何会话)看到历史里已存在消息 → 永不再次注入(跨重启、跨插件热重载都成立——判定只看会话日志本身)。
「全新会话」的判定:pre-step 时 agent.session.events 里还没有任何 user/message、assistant/message 或 tool/result 事件(第一步的在途提示词要等步骤进入后才持久化,time-context 的 README 明示这一点)。任何恢复/延续/已注入的会话都至少有一个这类事件 → 永久跳过。空批次(纯 runtime context、调度轮次等,没有 user 来源消息)也不会被注入。
设备信息在注入时采样(即新会话第一步的时刻),uptime/内存尽可能新鲜;消息是按会话的历史,会话之间字节差异零成本。
对「已有会话前缀」的明确结论
- 系统提示词:插件不注册段,
renderPrompt对已有会话的输出与安装前一模一样(集成测试断言了这一点); - 会话历史:已有会话不会被追加任何消息;
- 受影响面仅限新建会话:注入发生在它第一次请求之前,属于该会话自己的前缀,不存在旧缓存可供失效。
实现细节
- 零子进程:不调
sw_vers/uname,全部来自node:os、一次同步读/etc/os-release(Linux/WSL 取PRETTY_NAME,回退ID VERSION_ID)与Intl。 - 架构名按 neofetch 习惯映射:
x64→x86_64、arm64→aarch64、ia32→i386、arm→armv7l。 - macOS 从 Darwin release 反推营销版本(Darwin 24 ↔ macOS 15,19 ↔ 10.15,更老不猜)。
- 时区是宿主进程时区(IANA 名 + 当前 UTC 偏移,DST 感知);与
dsh-time-context的浏览器时区是两个概念。 - 注入消息满足 harness 的消息契约(
assertMessageEventShape:非空id、role: "user"、source.kind、content数组),source 形状与 time-context 的插件快照一致({ kind: 'plugin', plugin: 'where-am-i', form: 'snapshot', sections: [...] }),UI/轨迹归因一致。 - 监听器
inject: ["agents"](与 time-context 相同),树里没有 agents 注册表时不加载。
验证
node test/verify.mjs # 单测:os-release 解析/架构映射/macOS 命名/字节与时间格式化/时区偏移/配置归一化/渲染子集/决策门(user 来源、历史门、子代理门)/消息形状/不重复 cwd/date + 本机真实快照
node test/smoke-apply.mjs # 接线:全新会话注入一次、已有会话零注入、同一会话永不重注入、无 user 来源不注入、reject/abort 透传、子代理门、fields 子集
node test/integration-real.mjs # 真实 cordis + dsh-system-prompt + dsh-session:系统提示词不被改动、注入消息被真实 Session 持久化并进入派生历史、同会话不再注入
说明与限制
- 快照在新会话第一步采样:该会话期间 uptime/内存/负载会过期(这正是 neofetch-at-session-start 语义;消息是历史,不再刷新)。
/etc/os-release缺失且非 Windows/macOS 时,os行回退到os.version()/os.type(),可能不如发行版名友好。user行不隐去:模型能看见运行 dsh 的本地用户名(与 neofetch 一致)。在意隐私可以fields里去掉user/host。- 注入的是宿主机信息;若 dsh 跑在容器/远程执行里,报告的是容器/远程内核,而非开发机(如 WSL 会显示 WSL 内核号)。
- 消息会占用每条请求的 token(compaction 遮蔽前);这也是为什么已有会话不加、子代理默认不加——只付一次、只付给真正的新对话。
LIMITATIONS
已知限制
- The snapshot is sampled at each new session's **first step**: uptime/memory go stale during that session (neofetch-at-session-start semantics; the message is history and is not refreshed). - Without `/etc/os-release` on non-Windows/macOS systems, the `os` line falls back to `os.version()`/`os.type()` and may be less friendly than a distro name. - The `user` line is not redacted — the model can see the local username running dsh (like neofetch). Drop `user`/`host` via `fields` if you care about privacy. - Host-machine facts are reported; if dsh runs inside a container or remote execution, the kernel reported is the container's/remote one (e.g. WSL shows the WSL kernel). - The message costs tokens on every request until compaction hides it; this is why existing sessions and subagents are excluded by default — the cost is paid once, only by genuinely new conversations.
