dsh-global-system-prompt
Verifieddsh-global-system-prompt · v0.3.1 · MIT · Web UI
cc-switch-style global prompt for DeepSeek Harness (dsh 0.1.5): one user-editable block injected into the system prompt of every conversation, mirrored into each session project's AGENTS.md, and naming the harness's user-global AGENTS.md by absolute path
Install
dsh plugin add dsh-global-system-prompt Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-global-system-prompt
English | 中文
概述
dsh-global-system-prompt 给 DeepSeek Harness 补上 cc-switch 那个「全局提示词」:一段你自己写的文字,每一次对话都会带上,在 Web 设置面板里直接编辑,保存后下一轮对话即生效。
它的做法有两条通道:把这段文字注册成系统提示词里的一个独立段落(section),同时写进当前会话所在项目的 AGENTS.md。系统提示词那条保证 dsh 每一轮都带上;AGENTS.md 那条保证别的工具(Claude Code、Codex、Cursor、Zed……)和以后的会话也带上,并且跟着仓库走:
- 进的是系统提示词,位置固定可调,默认紧跟在 harness 身份行之后、persona 之前(order
-900)。 - 对所有会话生效:根会话、preset 挂载的 agent、进程内 subagent 都会带上(外部 CLI 子代理如 Codex/Claude Code 有自己的提示词,不受影响)。
- 不会被 preset 遮蔽:段落名
user:global-prompt是插件独占的,agent preset 的 persona 只会覆盖deployment:persona-prefix,覆盖不到它。 - 改完即生效:每个 step 重新渲染时都会重新读文件,所以保存后下一次请求就带上新内容,不用重启 dsh。
- 消耗固定:内容不变就不会污染 prompt 前缀缓存;内容变了(或清空)只改变这一段。
- 告诉模型全局文件在哪:注入内容末尾会自动附一段文件位置说明,写出下面两个文件的绝对路径。dsh 自己注入的运行时上下文只把用户全局指令说成一句「AGENTS.md」(不给路径),模型为此经常满硬盘找;这段说明就是治这个的。
- 同时写进项目:每个会话开始时,它所在项目根目录的
AGENTS.md会被维护一段带标记的区块,正文与系统提示词那段完全一致(见下)。区块之外的内容插件的字节都不动。
Harness file locations (exact paths — read or edit these directly instead of searching for them):
- global prompt (injected by the dsh-global-system-prompt plugin): C:\Users\me\.dsh\global-prompt.md
- user-global instructions (the "AGENTS.md" the runtime context mentions): C:\Users\me\.dsh\AGENTS.md
于是模型(以及你自己)不用再猜「全局 AGENTS.md 到底是哪个」:要读要改,直接照这个路径操作即可。想关掉就 announcePaths: false;想换文案就 pathsNote。
两条通道可以各关各的:enabled: false 整行停用,syncProjectAgents: false 只停项目 AGENTS.md(系统提示词照旧)、announcePaths: false 只去掉文件位置说明。
目录
安装
从 npm 装(正式方式):
# Web 面板用的 profile
dsh plugin --profile web add dsh-global-system-prompt
# TUI profile 也可以装(没有 Web 服务器,只有注入与项目同步生效)
dsh plugin --profile dsh-tui add dsh-global-system-prompt
从 GitHub 装(想直接跟仓库走,或者钉住某个提交):
dsh plugin --profile web add github:YMRwithNoworry/dsh-global-system-prompt
# 钉住提交:仓库名后面接 #<commit-sha>
dsh plugin --profile web add github:YMRwithNoworry/dsh-global-system-prompt#<commit-sha>
从本地 checkout 装(开发/自用):
# 复制一份进 profile:改完源码要重装一次
dsh plugin --profile web add file:<本仓库路径>
# 或者软链一份:改完源码直接重启 dsh
dsh plugin --profile web add link:<本仓库路径>
包名是
dsh-global-system-prompt。npm 上另有一个别人的dsh-global-prompt(同名不同包,与本包无关),别装错了。
dsh plugin add 会把包加进 profile 的 dependencies,并把声明了 dsh.bundle 的包追加到 dsh.profile.bundles。新增 bundle 是启动边界,装完要重启一次 dsh;之后改提示词内容就完全不用重启了。
装完可以先不启动、只验证层是否合入:
dsh --profile web --dump-config | Select-String -Context 0,8 'global-system-prompt'
应该能看到一行 # == dsh-global-system-prompt 以及插入的行:
- id: global-prompt
name: dsh-global-system-prompt
config:
enabled: true
order: -900
...
使用
打开 设置 → 全局提示词,在文本框里写内容,保存。
- 保存后下一次请求(下一轮对话)生效,当前已经发出的那一轮不变。
- 文件默认是
~/.dsh/global-prompt.md(即$DSH_HOME/global-prompt.md),也可以直接用任何编辑器改它——面板上的「重新读取」按钮会重新拉取。 - 文件存在但内容为空 = 不注入提示词正文(文件位置说明仍会附加,见
announcePaths),这也是临时静音的办法;删掉文件则回到配置里的text兜底(默认为空)。 - 面板会显示提示词文件路径、用户全局指令文件路径与状态、段落位置 order、实际注入字节数,还有一个「实际注入预览」可以展开看模型读到的原文;文件超过
maxBytes(默认 64 KiB)时超出部分不会注入。 - 面板底部还有两行关于项目
AGENTS.md的状态:本进程同步过哪些项目、最近一次动作(已创建 / 已更新 / 已是最新 / 已移除 / 已跳过 / 失败),以及「写入项目的区块」预览。
同步到项目 AGENTS.md
系统提示词只影响 dsh。同一个仓库你还可能用别的工具打开,它们读的是项目里的 AGENTS.md;换个工具、换个终端、交给同事,规则就没了。所以插件把同一段提示词同时写进当前会话所在项目的 AGENTS.md:写一次,项目里所有工具都读得到,还跟着 git 走。
<!-- BEGIN dsh-global-system-prompt -->
<!--
Managed by the dsh-global-system-prompt plugin. The rules below are synced from:
C:\Users\me\.dsh\global-prompt.md
Edits inside this block are overwritten on the next sync; delete the block,
markers included, to stop syncing.
-->
Global rules — follow these in every task in this project:
(这里是全局提示词的原文)
<!-- END dsh-global-system-prompt -->
| 问题 | 行为 |
|---|---|
| 写哪个文件 | 从会话工作目录向上找到第一个带 .git 的目录当项目根,写它的 AGENTS.md;一路到盘符根都没有标记,就退回到会话工作目录本身(projectAgentsFallback: 'skip' 可改成"找不到就不写") |
| 文件不存在 | 自动创建(连同缺失的目录) |
| 已经有别人的内容 | 原样保留,区块追加在末尾;区块之外的内容插件一个字节都不动 |
| 什么时候写 | 会话开始写一次(agent/session-start——harness 保证它在首次组装提示词之前,所以第一个 step 就读得到),之后每个 step 复查一次(内容没变时只花一次 stat) |
| 提示词改了 | 下一次复查时换成新内容,不用重启 |
| 区块被手删 / 被合并冲突吃掉 | 下一步写回;想彻底停就删掉区块后把 syncProjectAgents 设为 false |
| 提示词被清空 | 区块被摘掉;如果那个 AGENTS.md 除了这个区块什么都没有(也就是它是插件建的),文件本身也会删掉 |
| 写不了 | 文件超过 1 MiB、标记只剩一半(被手改坏)、读写报错——都只记一条 warning 并保持文件原样,系统提示词照常注入,回合不会中断 |
几点说明:
- 指令性:区块开头那行
Global rules — follow these in every task in this project:由projectAgentsPreamble控制,用你自己的话写也行,比如projectAgentsPreamble: '全局规则,本项目所有任务都必须遵守:';设成''则只留正文。写进AGENTS.md的正文是未转义的原文(escapeBraces只作用于系统提示词段落那条通道),所以 JSON、{{placeholder}}这类内容在项目文件里长什么样就是什么样。 - 会有两份:在 dsh 会话里同一段规则会出现两次——一次在系统提示词,一次是
AGENTS.md作为工作区指令注入的上下文(dsh-agent-instructions在首次组装时读它,之后按文件变更重投影)。这是刻意的:前者是 dsh 的实时通道,后者是跨工具通道。不想在项目里留痕就syncProjectAgents: false。 - 不碰用户的全局文件:
$DSH_HOME/AGENTS.md是 dsh 自己和 dsh-global-rules 的地盘,插件只声明它的位置、绝不写它;万一项目根正好解析到它,插件会跳过并记一条 warning。 - 写入是原子的:先写同目录的临时文件再 rename,harness 正在读
AGENTS.md时不会读到半截内容;有多个 dsh 进程时也不会互相截断。
配置
配置写在行里。默认值如下(也就是本包的 cordis.patch.yml):
- id: global-prompt
config:
enabled: true
# file: ~/.dsh/global-prompt.md # 默认 $DSH_HOME/global-prompt.md;支持 ~ / ~/ / ~\
order: -900
text: ''
maxBytes: 65536
escapeBraces: true
announcePaths: true
# instructionsFile: ~/.dsh/AGENTS.md # 默认 $DSH_HOME/AGENTS.md;'' 表示不提
includeInstructions: false
pathsNote: ''
syncProjectAgents: true
# projectAgentsFileName: AGENTS.md # 写进项目根的这个文件;只能是文件名
# projectRootMarkers: ['.git'] # 向上找项目根用的标记
# projectAgentsFallback: cwd # 找不到标记:cwd 写会话目录本身 / skip 不写
# projectAgentsPreamble: 'Global rules — follow these in every task in this project:'
| 字段 | 默认值 | 含义 |
|---|---|---|
enabled |
true |
总开关。false 时不注册段落、也不写项目 AGENTS.md(面板路由仍在,方便你先写好内容再打开) |
file |
$DSH_HOME/global-prompt.md |
提示词文件;相对路径按进程工作目录解析 |
order |
-900 |
段落位置。-1000 是 harness 身份行,0 是部署 persona 前缀,500 是 plan 策略;越小越靠前 |
text |
'' |
仅当文件不存在时使用的兜底文本 |
maxBytes |
65536 |
注入内容的 UTF-8 字节上限,超出部分截断(面板会提示) |
escapeBraces |
true |
中和文本里的 {{(见下);只影响系统提示词那条通道 |
announcePaths |
true |
在末尾附加「文件位置说明」,写出下面两个文件的绝对路径(模型不必再找) |
instructionsFile |
$DSH_HOME/AGENTS.md |
dsh 自己的用户全局指令文件;用于位置说明。设为 '' 则完全不提它 |
includeInstructions |
false |
把该文件的内容也并进这一段。默认关闭,因为 base 系 profile 里 dsh-agent-instructions 已经它会作为上下文注入;只有在没有挂那个插件的组装(比如自建 sdk-minimal)里才需要打开 |
pathsNote |
'' |
自定义位置说明文案(原样使用、不支持占位符)。留空则用生成的英文说明 |
syncProjectAgents |
true |
把提示词同步进会话项目的 AGENTS.md(见上)。false 只保留系统提示词那条通道 |
projectAgentsFileName |
AGENTS.md |
项目文件的名字,只能是裸文件名(想写 CLAUDE.md 就改这里) |
projectRootMarkers |
['.git'] |
向上找项目根时的标记名;标记文件或目录都算 |
projectAgentsFallback |
cwd |
一路没找到标记时:cwd 用会话工作目录本身,skip 干脆不写 |
projectAgentsPreamble |
Global rules — follow these in every task in this project: |
区块开头那行给模型看的指令,原样写入;'' 只留正文 |
想覆盖某项时,在 profile 的 ~/.dsh/profiles/<name>/cordis.patch.yml 里按 id 改——patch 会整体替换该行的 config,所以要写全:
- id: global-prompt
config:
enabled: true
order: -1100 # 放到身份行之前
file: ~/.dsh/my-global-prompt.md
text: ''
maxBytes: 65536
escapeBraces: true
syncProjectAgents: true
projectAgentsPreamble: '全局规则,本项目所有任务都必须遵守:'
关于 escapeBraces(重要)
系统提示词段落会把 {{name}} 当作变量插值,遇到未注册的变量名会抛错并中断这一轮,而且没有转义语法。所以如果你会在全局提示词里写 JSON、模板、{{placeholder}} 这类内容,保持 escapeBraces: true(默认):插件会把每个 {{ 中间插入一个零宽空格(模型看不到,人眼也看不到),插值就不会命中。确实需要原样注入时才设为 false。
与 AGENTS.md / dsh-global-rules 的关系
dsh 内置的 dsh-agent-instructions 已经把 ~/.dsh/AGENTS.md 注入到每个会话——但那是用户消息形式的上下文(durable context),不是系统提示词;dsh-global-rules 正是那个文件的可视化编辑器。本插件是另一条通道:
~/.dsh/AGENTS.md(内置 + dsh-global-rules) |
本插件 | |
|---|---|---|
| 注入形式 | 用户消息(runtime context) | 系统提示词段落 |
| 位置 | 首批请求前的 durable 消息 | order -900,紧跟身份行 |
| 生效时机 | 新会话立即;当前会话要等文件操作后的重投影 | 下一个 step(不用重启,也不用等文件操作) |
| 与压缩/历史 | 参与消息历史的协调 | 每步重新渲染,不受历史压缩影响 |
| 编辑界面 | 「全局规则」 | 「全局提示词」 |
| 告诉模型文件在哪 | 只说「存在 AGENTS.md」,不给路径 | 直接写出绝对路径(announcePaths) |
两者可以并存互不冲突(段落名不同)。只用其中一个也完全可以。
而项目里的 AGENTS.md 是第三条通道,也是唯一跨工具的那条:它是 dsh-agent-instructions 的工作区指令来源(从项目根一路读到 cwd,AGENTS.md / CLAUDE.md 都认),别的 AI 工具也按同一套约定读它。本插件的 syncProjectAgents 把提示词写进这个文件的一个托管区块里——于是在 dsh 里这段规则会出现两次(系统提示词 + 工作区指令),在别的工具里也照样生效。
为什么需要「告诉模型文件在哪」:dsh 注入的那句运行时提示是
A user-global instruction file exists: AGENTS.md. Do NOT assume their content.—— 名字有了、路径没给。模型于是会去猜~/AGENTS.md、.agents/AGENTS.md、工作区根…… 找不到就乱读一通。本插件的announcePaths把那两个真实路径直接摆进系统提示词。
实现
dsh-global-system-prompt/
├── package.json # dsh.bundle.patch + dsh.client(web)
├── cordis.patch.yml # 插入 global-prompt 行
├── lib/
│ ├── index.js # Cordis 插件:注册 system prompt 段落 + 挂 webServer 路由 + 接会话生命周期
│ ├── config.js # 配置校验/归一化,harness home 解析
│ ├── prompt.js # 文件读取(mtime 缓存、限额截断)、`{{` 中和、原子写入
│ ├── project.js # 项目 AGENTS.md:找项目根、托管区块增删改、每步复查
│ ├── route.js # GET/POST /global-prompt(同源校验、体积上限)
│ └── client.js # 浏览器侧 __ModuleLoader__ bundle(无构建步骤)
├── dev/
│ └── e2e-installed.mjs # 对着已安装的 dsh 跑端到端校验
├── test/ # node --test
└── examples/
要点:
- 段落注册:
ctx.systemPrompt.section({ name: 'user:global-prompt', order, text }),text是函数,每次 assemble 时求值——这是「改完即生效」的实现方式。段落名必须唯一:deployment:persona-prefix由提示词注册表自己占用,重名会直接抛错。 - 可选依赖:插件顶层只声明
inject = ['systemPrompt'];webServer用ctx.inject(['webServer'], …)等——如果声明在顶层,没有 Web 服务器的 profile 会让整个插件停在 pending,提示词就永远不注入了。会话生命周期(agent/session-start、agent/pre-step)是事件不是服务,ctx.on直接接,不需要 inject。 - 失败不炸回合:读文件失败只记一条 warning 并退回落兜底文本,绝不从
text()抛错;project.js同理,任何异常都吞成 warning + 一条状态记录。 - 文件位置说明:
prompt.js的pathsNote()生成那段 footer,两个文件各读一次stat(同样带 mtime 缓存),所以「已存在 / 尚未创建」是实时状态。 - 项目同步的时机:
agent/session-start是 harness 保证在首次组装提示词之前发出的同步通知(agent.session.header.cwd此时已就绪),所以第一个 step 就能读到写好的AGENTS.md;agent/pre-step是 waterfall,插件await next()之后才复查,绝不改动PreStepDecision。稳态下每次复查只做一次statSync(记住「我写的区块 + 文件当时的 mtime/size」),不匹配才读文件。 - 只在标记之间动手:
upsertManagedBlock认标记不认位置,能在文件任意位置替换、把重复区块收敛成一个、保留 CRLF 与 BOM;标记只剩一半(被手改坏)时拒绝写入并 warning,宁可不同步也不吃用户的内容。 - 面板路由:
GET /global-prompt返回文件内容、两个文件的路径与状态、本回合实际注入的原文、以及项目同步状态与要写入的区块原文(面板据此预览),POST校验同源后原子写入(临时文件 + rename)。
开发
cd <本仓库路径>
node --test test/ # 111 个用例
node --check lib/client.js # client bundle 语法检查(未构建,手写 __ModuleLoader__ 工厂)
改完源码后重新装一次即可让 profile 用上新代码:
dsh plugin --profile web add file:<本仓库路径>
或者用 link: 装一份链接,改完直接重启 dsh:
dsh plugin --profile web add link:<本仓库路径>
改包名 / 发到自己的 scope
包名散落在四个有加载语义的地方(package.json、cordis.patch.yml 的行 name、lib/client.js 的 __ModuleLoader__.load({ id })、日志与报错前缀),漏改其中任何一个都不会立刻报错但会静默失效。用带改名工具一次改完:
node dev/rename-package.mjs # 看当前名字都被哪些文件携带
node dev/rename-package.mjs @你的用户名/dsh-global-system-prompt # 改(先 --dry-run 预览)
node --test test/ # 改完自测
工具只替换「完整包名」这个 token,不会动 Cordis 插件名(global-prompt)和段落名(user:global-prompt)——这两者是运行时身份,与包名无关。
发布:
npm login # 需要你自己的 npm 凭证
npm publish # publishConfig.access 已设为 public(scoped 包必须)
prepublishOnly 会先跑一遍测试,测不过发不出去。
已知限制
- 只在 dsh 0.1.5 系列验证过;段落插值规则、
settings.section槽位契约、agent/session-start/agent/pre-step的载荷若在未来版本变化,需要跟着改。 - 面板是全局一段,没有 cc-switch 那种「按 provider 分别配置」的多档位;需要多份就装多份(改
file与order)或直接用行配置切。 - 注入的是提示词文本,不做权限/沙箱方面的任何事。
syncProjectAgents会真的改你的工作区:会在项目里新建/修改AGENTS.md(git diff 里会看到它),这是它的目的。不想要就syncProjectAgents: false,或在projectAgentsFallback: 'skip'下让没有.git的目录被跳过。- 项目同步以会话工作目录为准:一个进程里不同会话开着不同项目,就各写各的;
cwd变化的会话(理论上没有)不会重挑项目根。 - 面板上的项目同步状态是进程内的:重启 dsh 后列表从空开始,文件里的区块不受影响。