dsh-date-wrapper 0.1.1-beta.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.ja.md +43 -0
- package/CHANGELOG.ko.md +43 -0
- package/CHANGELOG.md +53 -0
- package/HANDOVER.md +215 -0
- package/INSTALL.ja.md +115 -0
- package/INSTALL.ko.md +115 -0
- package/INSTALL.md +115 -0
- package/INSTALL.zh.md +115 -0
- package/LICENSE +21 -0
- package/README.ja.md +212 -0
- package/README.ko.md +212 -0
- package/README.md +225 -0
- package/README.zh.md +223 -0
- package/cordis.patch.yml +16 -0
- package/docs/dsh-session-and-context-mechanics.md +387 -0
- package/package.json +59 -0
- package/src/format.js +119 -0
- package/src/index.js +49 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# dsh-date-wrapper
|
|
2
|
+
|
|
3
|
+
- [English README](./README.md)
|
|
4
|
+
- [中文 README](./README.zh.md)
|
|
5
|
+
- [日本語 README](./README.ja.md)
|
|
6
|
+
- [한국어 README](./README.ko.md)
|
|
7
|
+
- [Installation guide](./INSTALL.md)
|
|
8
|
+
- [中文安装指南](./INSTALL.zh.md)
|
|
9
|
+
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
10
|
+
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
11
|
+
- [Changelog](./CHANGELOG.md)
|
|
12
|
+
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
|
+
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
|
+
|
|
15
|
+
> 최소한의 날짜 한 줄: DSH가 이미 전송하고 있는 런타임 컨텍스트 스냅샷에 `Current date: 2026-09-08 Asia/Shanghai Tuesday`(46 characters, ~12 tokens)를 얹습니다.
|
|
16
|
+
> `@deepseek-ai/dsh-time-context`를 로드하지 **않고**, 추가 세션 메시지를 넣지 **않으며**, DSH 소스를 패치하지 **않고**, PR도 필요하지 않습니다.
|
|
17
|
+
|
|
18
|
+
- [동작 원리: DSH 세션, JSONL, 요청 조립](./docs/dsh-session-and-context-mechanics.md) (중국어)
|
|
19
|
+
- [HANDOVER.md](./HANDOVER.md) (중국어)
|
|
20
|
+
|
|
21
|
+
## 이 플러그인이 해결하는 문제
|
|
22
|
+
|
|
23
|
+
DSH 자체의 `@deepseek-ai/dsh-time-context`는 매 요청마다 약 **280 characters**의 메타데이터를 주입합니다:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Time sampled while preparing turn 3, step 2: 2026-09-08T16:05:36+08:00[Asia/Shanghai]
|
|
27
|
+
Browser time zone for this request: Asia/Shanghai. Interpret otherwise-unqualified dates and times in this zone.
|
|
28
|
+
Elapsed since the preceding model-visible message: 2m 34s.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
이 플러그인은 같은 정보를 단일 **46 characters** 길이의 한 줄로 압축하고, 그것이 도달하는 위치를 옮깁니다 — 더 이상 메시지 스트림으로 들어가지 않습니다:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Current date: 2026-09-08 Asia/Shanghai Tuesday
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| 항목 | `dsh-time-context` | `dsh-date-wrapper` |
|
|
38
|
+
|-----------|--------------------|--------------------|
|
|
39
|
+
| 주입되는 텍스트 | ~280 characters | 46 characters (↓84%), ~12 tokens |
|
|
40
|
+
| 도달 지점 | 사전 단계마다 메시지 하나(`user/message`) | 플랫폼 런타임 컨텍스트 스냅샷(`systemPrompt.context`) |
|
|
41
|
+
| 빈도 | 해당되는 각 단계마다 이벤트 하나 | 텍스트가 바뀔 때만 스냅샷과 함께 재전송됨(하루 동안 0 이벤트) |
|
|
42
|
+
| 의존성 | `agents` 서비스 | `systemPrompt` 서비스 |
|
|
43
|
+
| 런타임 의존성 | — | 없음 |
|
|
44
|
+
|
|
45
|
+
## 버전 호환성
|
|
46
|
+
|
|
47
|
+
| 항목 | 판정 |
|
|
48
|
+
|------|---------|
|
|
49
|
+
| 대상 DSH 버전 | 0.1.0-rc.7 → 0.1.3-alpha.2 (계약 안정, 아래 표 참조) |
|
|
50
|
+
| settings API | **해당 없음**: 이 플러그인은 settings를 등록하지 않고 schemastery `Config`도 내보내지 않습니다 |
|
|
51
|
+
| 사용하는 계약 지점 | 정확히 하나 — `systemPrompt.context()` |
|
|
52
|
+
| 네이티브 기능과의 충돌 | `@deepseek-ai/dsh-time-context`와 중복됩니다; **둘 다 사용하지 마십시오**. 기본 미설치 = 기본 꺼짐 |
|
|
53
|
+
| 브라우저 절반 | **없음**: 슬롯 없음, DOM 없음, CSS 시맨틱 토큰 없음 |
|
|
54
|
+
| DSH 패키지 임포트 | **0건**: `@deepseek-ai/*`에서 아무것도 가져오지 않으며, 이는 "런타임 감지 + 이중 API 폴백" 패턴보다 더 엄격합니다 |
|
|
55
|
+
|
|
56
|
+
| 계약 지점 | 0.1.0-rc.7 | 0.1.1-rc.2 | 0.1.2-rc.1 | 0.1.3-alpha.2 |
|
|
57
|
+
|---|---|---|---|---|
|
|
58
|
+
| `systemPrompt.context(ctx): () => void` | 예 | 예(이 호스트에서 검증됨) | 예 | 예 |
|
|
59
|
+
| `PromptContext = { name, order, text }`, `complete` 필드 없음 | 예 | 예 | 예 | 예 |
|
|
60
|
+
| `includeRuntimeContext` / `suppressRuntimeContext` | 예 | 예 | 예 | 예 |
|
|
61
|
+
| agent-loop의 `project()` 텍스트 중복 제거와 `surfaceOp: "append"` | 예 | 예 | 예 | 비교하지 않음 |
|
|
62
|
+
|
|
63
|
+
> 방법: `npm pack @deepseek-ai/dsh-system-prompt@<version>`으로 묶은 뒤 풀고 `lib/types/index.d.ts`와 `lib/index.js`를 비교합니다; `@deepseek-ai/dsh-agent-loop`도 같은 방식입니다.
|
|
64
|
+
> 이 호스트에서 **런타임에서** 검증된 것은 0.1.1-rc.2뿐이며, 0.1.2-rc.1 / 0.1.3-alpha.2의 런타임 검증은 아직 대기 중입니다(`HANDOVER.md` §7 참조).
|
|
65
|
+
|
|
66
|
+
## 메시지가 아니라 런타임 컨텍스트 스냅샷인 이유
|
|
67
|
+
|
|
68
|
+
첫 번째 시도는 `dsh-time-context`를 복사해 `agent/pre-step`에서 `user/message`를 덧붙이는 방식이었습니다. 측정된 비용이 너무 컸습니다: JSONL 이벤트 하나가 **339 bytes**이고(텍스트는 그중 46밖에 되지 않는데, `content`와 `sections`가 각각 사본을 저장하기 때문입니다) 이를 **매 턴마다** 하나씩 기록했습니다.
|
|
69
|
+
|
|
70
|
+
대신 런타임 컨텍스트를 등록하면 날짜가 플랫폼이 이미 보내는 스냅샷 메시지에 접혀 들어갑니다:
|
|
71
|
+
|
|
72
|
+
- 플랫폼은 **텍스트로 스냅샷을 중복 제거**하므로(`dsh-agent-loop`의 `RuntimeContextProjection.project()`: `if (this.retained?.text === snapshot) return`), 날짜가 바뀌지 않은 동안에는 **추가 이벤트가 단 하나도 기록되지 않습니다**;
|
|
73
|
+
- 스냅샷은 제자리에서 다시 쓰지 않고 **새 메시지를 추가**(`surfaceOp: 'append'`)하므로 요청 시퀀스는 늘어나기만 합니다 → **프리픽스 캐시가 보존됩니다**;
|
|
74
|
+
- 우리의 한계 비용은 그 46 bytes이며, 텍스트가 바뀌어 스냅샷이 재전송될 때만 발생합니다.
|
|
75
|
+
|
|
76
|
+
이 호스트에서 측정한 값(실제 세션 하나, 10턴 / 231 steps):
|
|
77
|
+
|
|
78
|
+
| 항목 | 측정값 |
|
|
79
|
+
|------|----------|
|
|
80
|
+
| 플랫폼 런타임 컨텍스트 스냅샷 | 2 이벤트, 각각 1133 B, 총 2.3 KB |
|
|
81
|
+
| 실제 사용자 메시지 | 10 이벤트, 각각 396 B |
|
|
82
|
+
| 예전 방식(턴마다 메시지 하나) | 10 × 339 B ≈ 3.4 KB |
|
|
83
|
+
| 이 방식 | 추가 이벤트 0건; 기존 스냅샷에 ~46 B가 접혀 들어감 |
|
|
84
|
+
|
|
85
|
+
## 설정
|
|
86
|
+
|
|
87
|
+
`cordis.patch.yml`과 함께 배포되며, 변경한 뒤에는 재시작하십시오:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
- insert:
|
|
91
|
+
- id: date-wrapper
|
|
92
|
+
name: dsh-date-wrapper
|
|
93
|
+
config:
|
|
94
|
+
timeZone: Asia/Shanghai # IANA zone; omit to use the process zone
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- 잘못된 `timeZone`은 시작 시 예외를 던집니다(**UTC로의 조용한 폴백은 없습니다**).
|
|
98
|
+
- 텍스트에 표시되는 존 이름은 해석된 IANA 이름입니다(`timeZone`을 생략하면 프로세스 존 이름).
|
|
99
|
+
- 런타임 컨텍스트 항목의 이름은 `date-wrapper:date`이고 순서는 `116`입니다(이미 사용 중: 110 sandbox, 115 approval, 120 subagent).
|
|
100
|
+
- 이 플러그인은 **schemastery `Config`를 내보내지 않으므로** 설정이 호스트 스키마 검증을 건너뜁니다; 모든 것은 `validateConfig()`에서 수동으로 검증됩니다. 그래서 Settings → Plugins 페이지에 이 플러그인의 설정 폼이 없습니다.
|
|
101
|
+
|
|
102
|
+
## 켜기/끄기: 플러그인의 활성화가 곧 스위치이며, 패널 토글은 없습니다
|
|
103
|
+
|
|
104
|
+
이 플러그인은 설정 패널 토글이 **없고** `enabled` 설정 필드도 **없습니다**. 이유는 다음과 같습니다:
|
|
105
|
+
|
|
106
|
+
- 기능 스위치는 *플러그인 행이 활성인지 여부* 그 자체입니다. 비활성 → `apply()`가 실행되지 않음 → 런타임 컨텍스트 항목이 존재하지 않음 → 단 한 글자도 주입되지 않습니다.
|
|
107
|
+
- 브라우저 절반(`dsh.client`)이 없으므로 UI가 소유한 우리 위젯도 없습니다.
|
|
108
|
+
- DSH 내장 **Settings → Plugins** 페이지가 이미 각 항목을 `enabled / disabled`로 표시합니다(읽기 전용).
|
|
109
|
+
|
|
110
|
+
### 끄는 방법
|
|
111
|
+
|
|
112
|
+
**자신의** 프로필 패치 레이어에서 `id`로 재정의하십시오 — `C:\Users\<you>\.dsh\profiles\web\cordis.patch.yml`:
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
- id: date-wrapper
|
|
116
|
+
disabled: true # disabled; set back to false to restore
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- **핫, 재시작 불필요**: 해당 파일은 Cordis HMR이 감시하며, `disabled: true`는 그 행의 파이버를 직접 폐기합니다.
|
|
120
|
+
- `date-wrapper` 행이 아직 없으면(미설치) 이 패치는 `entry "date-wrapper" not found` 경고만 기록하고 시작은 계속 성공합니다.
|
|
121
|
+
- ⚠️ 파일은 반드시 **최상위 YAML 배열**이어야 합니다; 형식이 잘못되면 **시작이 실패합니다**(DSH는 사용자 패치 레이어에 대해 fail-loud입니다).
|
|
122
|
+
|
|
123
|
+
### 완전히 제거하는 방법
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
dsh plugin --profile web remove dsh-date-wrapper
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
제거는 번들 레이어를 거치며 dsh web의 **재시작이 필요합니다**(번들 패치는 핫 리로드되지 않습니다).
|
|
130
|
+
|
|
131
|
+
## 설치
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
dsh plugin --profile web add github:drscrewdriver/dsh-date-wrapper
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
dsh web을 재시작하고 페이지를 새로 고치십시오. 로컬 경로 / 링크 모드 / 문제 해결은 [INSTALL.ko.md](./INSTALL.ko.md)를 참조하십시오.
|
|
138
|
+
|
|
139
|
+
## 검증
|
|
140
|
+
|
|
141
|
+
| # | 방법 | 기대 결과 |
|
|
142
|
+
|---|-----|----------|
|
|
143
|
+
| A1 | 새 세션을 열고 메시지 하나를 전송 | 런타임 컨텍스트 스냅샷에 `Current date: YYYY-MM-DD <zone> <weekday>`가 포함됨(`system-prompt`에서 온 주입된 컨텍스트 행으로 표시됨) |
|
|
144
|
+
| A2 | 그 줄을 확인 | ≤50 characters (46 measured; 요청된 형식 때문에 PRD 임계값 30은 완화됨) |
|
|
145
|
+
| A3 | 플러그인 비활성화(프로필 패치 `disabled: true`) | 이후 세션의 스냅샷에 그 줄이 더 이상 나타나지 않음 |
|
|
146
|
+
| A4 | 세션 로그 검색 | `Time sampled` / `Elapsed since` / `Browser time zone` 없음 |
|
|
147
|
+
| A5 | `timeZone`을 `UTC`로 설정하고 재시작 | 날짜가 UTC를 따름(존 경계를 넘을 때 하루 차이가 날 수 있음) |
|
|
148
|
+
|
|
149
|
+
## 구현 노트
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
dsh-date-wrapper/
|
|
153
|
+
├── package.json # name / type: module / main / exports["."] / dsh.bundle.patch / files
|
|
154
|
+
├── cordis.patch.yml # one insert row (no patch-level id → lands at the profile root = host plane)
|
|
155
|
+
├── src/
|
|
156
|
+
│ ├── format.js # pure functions: resolveZone / renderDate / createDateContextText / validateConfig / TEXT_LABEL
|
|
157
|
+
│ └── index.js # apply(ctx, config) → ctx.inject(['systemPrompt'], …) → systemPrompt.context(...)
|
|
158
|
+
└── tests/
|
|
159
|
+
├── format.test.mjs # 11 cases (zone projection, weekday, format and length, degradation, config validation)
|
|
160
|
+
└── context.test.mjs # 7 cases (registration contract against a fake ctx)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **호스트 플레인 행**: `ctx.inject(['systemPrompt'], …)`가 자식 파이버를 열고, 서비스가 없으면 이 플러그인은 부팅 전체를 실패시키는 대신 조용히 아무것도 등록하지 않습니다.
|
|
164
|
+
- **fail-soft 텍스트 프로바이더**: 프롬프트 조립 중에 예외가 발생하면 **모든** 요청이 실패하므로, 렌더 실패 시 빈 문자열을 반환합니다(플랫폼이 빈 텍스트를 걸러냅니다).
|
|
165
|
+
- **`complete` 없음**: 이를 설정하면 전체 시스템 프롬프트를 가리게 됩니다.
|
|
166
|
+
- **중복 제거는 플랫폼의 몫**: 에이전트별 상태를 유지하지 않으며, 자정을 넘기면 스냅샷이 그냥 새 날짜를 담습니다.
|
|
167
|
+
- **수명 주기**: 등록은 `ctx.inject` 자식 파이버에 속하며 플러그인이 비활성화될 때 회수됩니다.
|
|
168
|
+
|
|
169
|
+
## 개발: TDD + lint
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm install # devDependencies only (eslint / @eslint/js); zero runtime dependencies
|
|
173
|
+
|
|
174
|
+
npm run tdd # watch mode: rerun on src/ or tests/ changes (node --test --watch)
|
|
175
|
+
npm test # one full run: node --test "tests/*.test.mjs"
|
|
176
|
+
node tests/format.test.mjs # run a single file (most reliable under a sandbox: no child process)
|
|
177
|
+
|
|
178
|
+
npm run lint # eslint . (src + tests + eslint.config.mjs)
|
|
179
|
+
npm run lint:fix # auto-fix what can be fixed
|
|
180
|
+
npm run verify # lint + test; run this before committing
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 레드-그린-리팩터
|
|
184
|
+
|
|
185
|
+
테스트 케이스는 수용 기준에 직접 대응합니다: 실패하는 어서션을 먼저 작성하고, 그다음 통과시키십시오.
|
|
186
|
+
|
|
187
|
+
| 단계 | 동작 | 명령 |
|
|
188
|
+
|------|--------|---------|
|
|
189
|
+
| 1 레드 | `tests/*.test.mjs`에 수용 기준의 이름을 딴 어서션을 추가하고, 아직 **없는** 동작을 어서션합니다 | `npm run tdd` |
|
|
190
|
+
| 2 그린 | 다른 어서션은 건드리지 않고 통과시키는 최소 구현을 `src/`에 작성합니다 | `npm run tdd` |
|
|
191
|
+
| 3 리팩터 | 초록 상태를 유지하면서 순수 함수의 이름을 바꾸고 추출합니다; `src/format.js`가 모든 순수 로직을 담고 `src/index.js`는 등록만 합니다 | `npm run tdd` |
|
|
192
|
+
| 4 게이트 | 커밋 전에 lint와 전체 스위트를 실행합니다 | `npm run verify` |
|
|
193
|
+
|
|
194
|
+
현재 18 assertions: `format.test.mjs`(11)는 순수 함수를 다루고, `context.test.mjs`(7)는 가짜 ctx에 대한 등록 계약을 어서션합니다.
|
|
195
|
+
|
|
196
|
+
### lint 설정 주요 사항
|
|
197
|
+
|
|
198
|
+
- ESLint 10 flat config(`eslint.config.mjs`)이며 `@eslint/js` recommended를 기준선으로 사용합니다.
|
|
199
|
+
- 강화된 규칙: `eqeqeq`, `prefer-const`, `object-shorthand`, `no-unused-vars`(`_` 접두사는 예외).
|
|
200
|
+
- Node 전역 `crypto` / `console` / `process`를 명시적으로 선언합니다. 그렇지 않으면 `no-undef`가 오탐합니다.
|
|
201
|
+
|
|
202
|
+
## 알려진 제한
|
|
203
|
+
|
|
204
|
+
- **고정 프롬프트 프리셋에서는 비활성**: 프리셋의 페르소나가 `includeRuntimeContext: false`를 설정하면(공식 `minimal`과 로컬 `simple-reply` 모두 그렇게 합니다), `assemble()`이 `contexts: []`를 반환하고 이 플러그인의 항목은 통째로 버려집니다. 그런 프리셋은 이후 리스너가 프롬프트에 아무것도 추가하지 못하도록 설계된 것입니다.
|
|
205
|
+
- **오래된 스냅샷은 히스토리에 남음**: 날짜가 바뀌면 플랫폼은 새 스냅샷을 추가하고(오래된 것은 유지됩니다) 새 스냅샷은 자체적인 "This snapshot supersedes earlier runtime-context snapshots" 선언을 통해 효력을 발휘합니다 — 플랫폼이 cwd / sandbox / approval 정책 변경을 다루는 방식과 같습니다.
|
|
206
|
+
- **번들 패치는 핫 리로드되지 않음**: `cordis.patch.yml`을 바꾸거나 플러그인을 업그레이드하려면 dsh web 재시작이 필요합니다(프로필 패치에서 `disabled`를 바꾸는 것은 핫입니다).
|
|
207
|
+
- **`dsh-time-context`는 로드되지도, 필터링되지도 않음**: 프리셋에서 이를 명시적으로 마운트하면 장황한 텍스트가 평소처럼 나타납니다. 둘 다 사용하지 마십시오.
|
|
208
|
+
- **계약 지점에 대한 런타임 프로브 없음**: `systemPrompt.context`는 가드 없이 호출되므로, 향후 DSH 이름 변경은 조용한 성능 저하 대신 플러그인 로드 실패로 드러납니다(`HANDOVER.md` §7 참조).
|
|
209
|
+
|
|
210
|
+
## 라이선스
|
|
211
|
+
|
|
212
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# dsh-date-wrapper
|
|
2
|
+
|
|
3
|
+
- [English README](./README.md)
|
|
4
|
+
- [中文 README](./README.zh.md)
|
|
5
|
+
- [日本語 README](./README.ja.md)
|
|
6
|
+
- [한국어 README](./README.ko.md)
|
|
7
|
+
- [Installation guide](./INSTALL.md)
|
|
8
|
+
- [中文安装指南](./INSTALL.zh.md)
|
|
9
|
+
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
10
|
+
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
11
|
+
- [Changelog](./CHANGELOG.md)
|
|
12
|
+
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
|
+
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
|
+
|
|
15
|
+
> **▼ DSH version compatibility**
|
|
16
|
+
>
|
|
17
|
+
> | DSH version | Load | Host contract | Client half |
|
|
18
|
+
> | --- | --- | --- | --- |
|
|
19
|
+
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | ✅ | `systemPrompt.context({ name, order, text })` | — (host-only plugin) |
|
|
20
|
+
> | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ✅ | same signature, byte-identical | — (host-only plugin) |
|
|
21
|
+
>
|
|
22
|
+
> One artifact covers both: the plugin only calls `systemPrompt.context`, whose
|
|
23
|
+
> signature and semantics are unchanged between `dsh-v0.1.1-rc.2` and
|
|
24
|
+
> `dsh-v0.1.2-rc.1`. It registers no settings namespace, reads no session data and
|
|
25
|
+
> makes no RPC call, so the 0.1.1 → 0.1.2 client/session/persistence rewrites do
|
|
26
|
+
> not touch it.
|
|
27
|
+
|
|
28
|
+
> A minimal date line: it hangs `Current date: 2026-09-08 Asia/Shanghai Tuesday` (46 characters, ~12 tokens) onto the runtime-context snapshot DSH already sends.
|
|
29
|
+
> It does **not** load `@deepseek-ai/dsh-time-context`, does **not** add extra session messages, does **not** patch DSH source, and needs no PR.
|
|
30
|
+
|
|
31
|
+
- [How it works: DSH sessions, JSONL and request assembly](./docs/dsh-session-and-context-mechanics.md) (Chinese)
|
|
32
|
+
- [HANDOVER.md](./HANDOVER.md) (Chinese)
|
|
33
|
+
|
|
34
|
+
## What this plugin solves
|
|
35
|
+
|
|
36
|
+
DSH's own `@deepseek-ai/dsh-time-context` injects about **280 characters** of metadata on every request:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Time sampled while preparing turn 3, step 2: 2026-09-08T16:05:36+08:00[Asia/Shanghai]
|
|
40
|
+
Browser time zone for this request: Asia/Shanghai. Interpret otherwise-unqualified dates and times in this zone.
|
|
41
|
+
Elapsed since the preceding model-visible message: 2m 34s.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This plugin compresses the same information into a single **46-character** line and moves where it lands — it no longer goes into the message stream:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Current date: 2026-09-08 Asia/Shanghai Tuesday
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Dimension | `dsh-time-context` | `dsh-date-wrapper` |
|
|
51
|
+
|-----------|--------------------|--------------------|
|
|
52
|
+
| Injected text | ~280 characters | 46 characters (↓84%), ~12 tokens |
|
|
53
|
+
| Landing point | One message per pre-step (`user/message`) | The platform runtime-context snapshot (`systemPrompt.context`) |
|
|
54
|
+
| Frequency | One event per eligible step | Re-sent with the snapshot only when the text changes (0 events within a day) |
|
|
55
|
+
| Dependency | `agents` service | `systemPrompt` service |
|
|
56
|
+
| Runtime dependencies | — | none |
|
|
57
|
+
|
|
58
|
+
## Version compatibility
|
|
59
|
+
|
|
60
|
+
| Item | Verdict |
|
|
61
|
+
|------|---------|
|
|
62
|
+
| Target DSH versions | 0.1.0-rc.7 → 0.1.3-alpha.2 (contract stable, see the table below) |
|
|
63
|
+
| settings API | **Not applicable**: the plugin registers no settings and exports no schemastery `Config` |
|
|
64
|
+
| Contract points used | Exactly one — `systemPrompt.context()` |
|
|
65
|
+
| Conflict with a native feature | Overlaps `@deepseek-ai/dsh-time-context`; **do not use both**. Not installed by default = off by default |
|
|
66
|
+
| Browser half | **None**: no slot, no DOM, no CSS semantic tokens |
|
|
67
|
+
| DSH package imports | **Zero**: nothing from `@deepseek-ai/*`, which is stricter than the "runtime detection + dual API fallback" pattern |
|
|
68
|
+
|
|
69
|
+
| Contract point | 0.1.0-rc.7 | 0.1.1-rc.2 | 0.1.2-rc.1 | 0.1.3-alpha.2 |
|
|
70
|
+
|---|---|---|---|---|
|
|
71
|
+
| `systemPrompt.context(ctx): () => void` | yes | yes (verified on this host) | yes | yes |
|
|
72
|
+
| `PromptContext = { name, order, text }`, no `complete` field | yes | yes | yes | yes |
|
|
73
|
+
| `includeRuntimeContext` / `suppressRuntimeContext` | yes | yes | yes | yes |
|
|
74
|
+
| agent-loop `project()` text dedupe and `surfaceOp: "append"` | yes | yes | yes | not compared |
|
|
75
|
+
|
|
76
|
+
> Method: `npm pack @deepseek-ai/dsh-system-prompt@<version>`, unpack, and compare `lib/types/index.d.ts` and `lib/index.js`; the same for `@deepseek-ai/dsh-agent-loop`.
|
|
77
|
+
> Only 0.1.1-rc.2 has been verified **at runtime** on this host; runtime verification on 0.1.2-rc.1 / 0.1.3-alpha.2 is still pending (see `HANDOVER.md` §7).
|
|
78
|
+
|
|
79
|
+
## Why a runtime-context snapshot instead of a message
|
|
80
|
+
|
|
81
|
+
The first attempt copied `dsh-time-context` and appended a `user/message` in `agent/pre-step`. Measured cost was too high: each JSONL event is **339 bytes** (the text is only 46 of them, because `content` and `sections` each store a copy) and it wrote one **every turn**.
|
|
82
|
+
|
|
83
|
+
Registering a runtime context instead folds the date into the snapshot message the platform already sends:
|
|
84
|
+
|
|
85
|
+
- The platform **deduplicates snapshots by text** (`RuntimeContextProjection.project()` in `dsh-agent-loop`: `if (this.retained?.text === snapshot) return`), so while the date is unchanged **not a single extra event is written**;
|
|
86
|
+
- Snapshots **append** a new message (`surfaceOp: 'append'`) rather than rewriting in place, so the request sequence only grows → **the prefix cache is preserved**;
|
|
87
|
+
- Our marginal cost is those 46 bytes, and only when the snapshot is re-sent because its text changed.
|
|
88
|
+
|
|
89
|
+
Measured on this host (one real session, 10 turns / 231 steps):
|
|
90
|
+
|
|
91
|
+
| Item | Measured |
|
|
92
|
+
|------|----------|
|
|
93
|
+
| Platform runtime-context snapshots | 2 events, 1133 B each, 2.3 KB total |
|
|
94
|
+
| Real user messages | 10 events, 396 B each |
|
|
95
|
+
| Old approach (one message per turn) | 10 × 339 B ≈ 3.4 KB |
|
|
96
|
+
| This approach | 0 extra events; ~46 B folded into an existing snapshot |
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
Shipped with `cordis.patch.yml`; restart after changing it:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
- insert:
|
|
104
|
+
- id: date-wrapper
|
|
105
|
+
name: dsh-date-wrapper
|
|
106
|
+
config:
|
|
107
|
+
timeZone: Asia/Shanghai # IANA zone; omit to use the process zone
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- An invalid `timeZone` throws at startup (**no** silent fallback to UTC).
|
|
111
|
+
- The zone name in the text is the resolved IANA name (the process zone name when `timeZone` is omitted).
|
|
112
|
+
- The runtime-context entry is named `date-wrapper:date` with order `116` (already taken: 110 sandbox, 115 approval, 120 subagent).
|
|
113
|
+
- The plugin exports **no schemastery `Config`**, so its config skips the host schema validation; everything is validated by hand in `validateConfig()`. That is also why the Settings → Plugins page has no config form for it.
|
|
114
|
+
|
|
115
|
+
## On/off: the plugin's activation is the switch, there is no panel toggle
|
|
116
|
+
|
|
117
|
+
The plugin ships **no** settings-panel toggle and **no** `enabled` config field, because:
|
|
118
|
+
|
|
119
|
+
- The feature switch *is* whether the plugin row is active. Inactive → `apply()` never runs → the runtime-context entry does not exist → not a single character is injected.
|
|
120
|
+
- There is no browser half (`dsh.client`), so the UI owns no widget of ours.
|
|
121
|
+
- DSH's built-in **Settings → Plugins** page already shows each entry as `enabled / disabled` (read-only).
|
|
122
|
+
|
|
123
|
+
### How to turn it off
|
|
124
|
+
|
|
125
|
+
Override it by `id` in **your own** profile patch layer — `C:\Users\<you>\.dsh\profiles\web\cordis.patch.yml`:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
- id: date-wrapper
|
|
129
|
+
disabled: true # disabled; set back to false to restore
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **Hot, no restart**: that file is watched by Cordis HMR, and `disabled: true` disposes the row's fiber directly.
|
|
133
|
+
- If the `date-wrapper` row does not exist yet (not installed), this patch only logs an `entry "date-wrapper" not found` warning; startup still succeeds.
|
|
134
|
+
- ⚠️ The file must be a **top-level YAML array**; if it is malformed, **startup fails** (DSH is fail-loud for user patch layers).
|
|
135
|
+
|
|
136
|
+
### How to remove it completely
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
dsh plugin --profile web remove dsh-date-wrapper
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Removal goes through the bundle layer and **requires a restart** of dsh web (bundle patches are not hot-reloaded).
|
|
143
|
+
|
|
144
|
+
## Install
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
dsh plugin --profile web add github:drscrewdriver/dsh-date-wrapper
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Restart dsh web and refresh the page. Local paths, link mode and troubleshooting: [INSTALL.md](./INSTALL.md).
|
|
151
|
+
|
|
152
|
+
## Verification
|
|
153
|
+
|
|
154
|
+
| # | How | Expected |
|
|
155
|
+
|---|-----|----------|
|
|
156
|
+
| A1 | Open a new session and send one message | The runtime-context snapshot contains `Current date: YYYY-MM-DD <zone> <weekday>` (shown as an injected context row sourced from `system-prompt`) |
|
|
157
|
+
| A2 | Check that line | ≤50 characters (46 measured; the PRD threshold of 30 was relaxed for the requested format) |
|
|
158
|
+
| A3 | Disable the plugin (profile patch `disabled: true`) | The line no longer appears in later sessions' snapshots |
|
|
159
|
+
| A4 | Search the session log | No `Time sampled` / `Elapsed since` / `Browser time zone` |
|
|
160
|
+
| A5 | Set `timeZone` to `UTC` and restart | The date follows UTC (may differ by one day across zone boundaries) |
|
|
161
|
+
|
|
162
|
+
## Implementation notes
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
dsh-date-wrapper/
|
|
166
|
+
├── package.json # name / type: module / main / exports["."] / dsh.bundle.patch / files
|
|
167
|
+
├── cordis.patch.yml # one insert row (no patch-level id → lands at the profile root = host plane)
|
|
168
|
+
├── src/
|
|
169
|
+
│ ├── format.js # pure functions: resolveZone / renderDate / createDateContextText / validateConfig / TEXT_LABEL
|
|
170
|
+
│ └── index.js # apply(ctx, config) → ctx.inject(['systemPrompt'], …) → systemPrompt.context(...)
|
|
171
|
+
└── tests/
|
|
172
|
+
├── format.test.mjs # 11 cases (zone projection, weekday, format and length, degradation, config validation)
|
|
173
|
+
└── context.test.mjs # 7 cases (registration contract against a fake ctx)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- **Host-plane row**: `ctx.inject(['systemPrompt'], …)` opens a child fiber; if the service is missing, the plugin silently registers nothing instead of failing the whole boot.
|
|
177
|
+
- **Fail-soft text provider**: throwing during prompt assembly would fail **every** request, so a render failure returns an empty string (the platform filters empty text out).
|
|
178
|
+
- **No `complete`**: setting it would shadow the entire system prompt.
|
|
179
|
+
- **Deduplication is the platform's job**: no per-agent state is kept; across midnight the snapshot simply carries the new date.
|
|
180
|
+
- **Lifecycle**: the registration belongs to the `ctx.inject` child fiber and is reclaimed when the plugin is deactivated.
|
|
181
|
+
|
|
182
|
+
## Development: TDD + lint
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm install # devDependencies only (eslint / @eslint/js); zero runtime dependencies
|
|
186
|
+
|
|
187
|
+
npm run tdd # watch mode: rerun on src/ or tests/ changes (node --test --watch)
|
|
188
|
+
npm test # one full run: node --test "tests/*.test.mjs"
|
|
189
|
+
node tests/format.test.mjs # run a single file (most reliable under a sandbox: no child process)
|
|
190
|
+
|
|
191
|
+
npm run lint # eslint . (src + tests + eslint.config.mjs)
|
|
192
|
+
npm run lint:fix # auto-fix what can be fixed
|
|
193
|
+
npm run verify # lint + test; run this before committing
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Red-green-refactor
|
|
197
|
+
|
|
198
|
+
Test cases map directly to acceptance criteria: write a failing assertion first, then make it pass.
|
|
199
|
+
|
|
200
|
+
| Step | Action | Command |
|
|
201
|
+
|------|--------|---------|
|
|
202
|
+
| 1 red | Add an assertion in `tests/*.test.mjs` named after the acceptance criterion, asserting the behaviour you do **not** have yet | `npm run tdd` |
|
|
203
|
+
| 2 green | Write the minimal implementation in `src/` to pass it without touching other assertions | `npm run tdd` |
|
|
204
|
+
| 3 refactor | Rename and extract pure functions while staying green; `src/format.js` holds all pure logic, `src/index.js` only registers | `npm run tdd` |
|
|
205
|
+
| 4 gate | Run lint + the full suite before committing | `npm run verify` |
|
|
206
|
+
|
|
207
|
+
18 assertions today: `format.test.mjs` (11) covers the pure functions, `context.test.mjs` (7) asserts the registration contract against a fake ctx.
|
|
208
|
+
|
|
209
|
+
### Lint configuration highlights
|
|
210
|
+
|
|
211
|
+
- ESLint 10 flat config (`eslint.config.mjs`) with `@eslint/js` recommended as the baseline.
|
|
212
|
+
- Tightened rules: `eqeqeq`, `prefer-const`, `object-shorthand`, `no-unused-vars` (`_` prefix exempt).
|
|
213
|
+
- Node globals `crypto` / `console` / `process` are declared explicitly, otherwise `no-undef` false-positives.
|
|
214
|
+
|
|
215
|
+
## Known limitations
|
|
216
|
+
|
|
217
|
+
- **Inactive under fixed-prompt presets**: if a preset's persona sets `includeRuntimeContext: false` (the official `minimal` and the local `simple-reply` both do), `assemble()` returns `contexts: []` and this plugin's entry is dropped wholesale. Those presets are designed to forbid later listeners from adding anything to the prompt.
|
|
218
|
+
- **Old snapshots stay in history**: when the date changes the platform appends a new snapshot (the old one is kept) and the new one takes effect through its own "This snapshot supersedes earlier runtime-context snapshots" declaration — the same way the platform handles cwd / sandbox / approval policy changes.
|
|
219
|
+
- **Bundle patches are not hot-reloaded**: changing `cordis.patch.yml` or upgrading the plugin requires a dsh web restart (changing `disabled` in the profile patch is hot).
|
|
220
|
+
- **`dsh-time-context` is neither loaded nor filtered**: if you mount it explicitly in a preset, its verbose text appears as usual. Do not use both.
|
|
221
|
+
- **No runtime probe for the contract point**: `systemPrompt.context` is called unguarded, so a future DSH rename would surface as a plugin load failure instead of a silent degradation (see `HANDOVER.md` §7).
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# dsh-date-wrapper
|
|
2
|
+
|
|
3
|
+
- [English README](./README.md)
|
|
4
|
+
- [中文 README](./README.zh.md)
|
|
5
|
+
- [日本語 README](./README.ja.md)
|
|
6
|
+
- [한국어 README](./README.ko.md)
|
|
7
|
+
- [Installation guide](./INSTALL.md)
|
|
8
|
+
- [中文安装指南](./INSTALL.zh.md)
|
|
9
|
+
- [日本語インストールガイド](./INSTALL.ja.md)
|
|
10
|
+
- [한국어 설치 안내](./INSTALL.ko.md)
|
|
11
|
+
- [Changelog](./CHANGELOG.md)
|
|
12
|
+
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
|
+
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
|
+
|
|
15
|
+
> **▼ DSH 版本适配**
|
|
16
|
+
>
|
|
17
|
+
> | DSH 版本 | 加载 | 宿主契约 | 客户端半 |
|
|
18
|
+
> | --- | --- | --- | --- |
|
|
19
|
+
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | ✅ | `systemPrompt.context({ name, order, text })` | —(纯宿主插件) |
|
|
20
|
+
> | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ✅ | 同一签名,字节一致 | —(纯宿主插件) |
|
|
21
|
+
>
|
|
22
|
+
> 一份产物同时支持两版本:插件只调用 `systemPrompt.context`,其签名与语义在
|
|
23
|
+
> `dsh-v0.1.1-rc.2` 与 `dsh-v0.1.2-rc.1` 之间未变。它不注册设置命名空间、不读会话
|
|
24
|
+
> 数据、不发 RPC,因此 0.1.1 → 0.1.2 的客户端/会话/持久化重写都与它无关。
|
|
25
|
+
|
|
26
|
+
> 精简版时间注入:把 `Current date: 2026-09-08 Asia/Shanghai Tuesday`(46 字符 ≈ 12 token)挂进 DSH 自带的运行上下文快照。
|
|
27
|
+
> 不加载 `@deepseek-ai/dsh-time-context`,不产生额外会话消息,不改 DSH 源码,不提 PR。
|
|
28
|
+
|
|
29
|
+
- [机制详解:DSH 会话、JSONL 与请求组装](./docs/dsh-session-and-context-mechanics.md)(中文)
|
|
30
|
+
- [交接文档 HANDOVER.md](./HANDOVER.md)(中文)
|
|
31
|
+
|
|
32
|
+
## 这个插件解决什么
|
|
33
|
+
|
|
34
|
+
DSH 自带的 `@deepseek-ai/dsh-time-context` 每次请求注入约 **280 字符**的元数据:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Time sampled while preparing turn 3, step 2: 2026-09-08T16:05:36+08:00[Asia/Shanghai]
|
|
38
|
+
Browser time zone for this request: Asia/Shanghai. Interpret otherwise-unqualified dates and times in this zone.
|
|
39
|
+
Elapsed since the preceding model-visible message: 2m 34s.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
本插件把同样的信息压成一行 **46 字符**,并且换了一个落点 —— 不再往消息流里塞:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Current date: 2026-09-08 Asia/Shanghai Tuesday
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| 维度 | `dsh-time-context` | `dsh-date-wrapper` |
|
|
49
|
+
|------|--------------------|--------------------|
|
|
50
|
+
| 注入文本 | ~280 字符 | 46 字符(↓84%),约 12 token |
|
|
51
|
+
| 落点 | 每条 pre-step 消息(`user/message`) | 平台运行上下文快照(`systemPrompt.context`) |
|
|
52
|
+
| 频率 | 每个 eligible step 一条 | 文本变化时随快照重发(同一天内 0 条) |
|
|
53
|
+
| 依赖 | `agents` 服务 | `systemPrompt` 服务 |
|
|
54
|
+
| 运行时依赖 | — | 零 |
|
|
55
|
+
|
|
56
|
+
## 版本适配与兼容性
|
|
57
|
+
|
|
58
|
+
| 项 | 结论 |
|
|
59
|
+
|---|---|
|
|
60
|
+
| 目标 DSH 版本 | 0.1.0-rc.7 → 0.1.3-alpha.2(契约稳定,见下表) |
|
|
61
|
+
| settings API | **不适用**:本插件不注册 settings,也不导出 schemastery `Config` |
|
|
62
|
+
| 使用的契约点 | 只有一个 —— `systemPrompt.context()` |
|
|
63
|
+
| 与原生功能冲突 | `@deepseek-ai/dsh-time-context` 功能重叠,**不要同时使用**。本插件默认不安装 = 默认关闭 |
|
|
64
|
+
| client 半 | **无**:不涉及 slot / DOM / CSS 语义 token |
|
|
65
|
+
| DSH 包 import | **零**:不 `import` 任何 `@deepseek-ai/*`,比「运行时检测 + 双 API 回退」更保守 |
|
|
66
|
+
|
|
67
|
+
| 契约点 | 0.1.0-rc.7 | 0.1.1-rc.2 | 0.1.2-rc.1 | 0.1.3-alpha.2 |
|
|
68
|
+
|---|---|---|---|---|
|
|
69
|
+
| `systemPrompt.context(ctx): () => void` | ✅ | ✅(本机实装验证) | ✅ | ✅ |
|
|
70
|
+
| `PromptContext = { name, order, text }`,无 `complete` 字段 | ✅ | ✅ | ✅ | ✅ |
|
|
71
|
+
| `includeRuntimeContext` / `suppressRuntimeContext` | ✅ | ✅ | ✅ | ✅ |
|
|
72
|
+
| agent-loop `project()` 按文本去重、`surfaceOp: "append"` | ✅ | ✅ | ✅ | 未比对 |
|
|
73
|
+
|
|
74
|
+
> 验证方式:`npm pack @deepseek-ai/dsh-system-prompt@<版本>` 解包后比对 `lib/types/index.d.ts` 与 `lib/index.js`;`@deepseek-ai/dsh-agent-loop` 同法。
|
|
75
|
+
> **运行时**只在本机 0.1.1-rc.2 上验证过;0.1.2-rc.1 / 0.1.3-alpha.2 的运行时验证仍待做(见 `HANDOVER.md` §7)。
|
|
76
|
+
|
|
77
|
+
## 为什么用运行上下文快照,而不是消息
|
|
78
|
+
|
|
79
|
+
最初的做法是学 `dsh-time-context`,在 `agent/pre-step` 里追加一条 `user/message`。实测下来太贵:每条 JSONL 事件 **339 字节**(文本只占 46 字节,`content` 与 `sections` 各存一份),而它**每轮**都会写一条。
|
|
80
|
+
|
|
81
|
+
改成注册运行上下文后,日期并入平台本来就有的那条快照消息:
|
|
82
|
+
|
|
83
|
+
- 平台对快照**按文本去重**(`dsh-agent-loop` 的 `RuntimeContextProjection.project()`:`if (this.retained?.text === snapshot) return`),所以日期不变时**一条事件都不多**;
|
|
84
|
+
- 快照是**追加**新消息(`surfaceOp: 'append'`),不是原地改写,请求序列只增长 → **不破坏前缀缓存**;
|
|
85
|
+
- 我们的边际成本只有那 46 字节,且只在快照因文本变化而重发时才被带上。
|
|
86
|
+
|
|
87
|
+
本机实测(一个 10 轮 / 231 步的真实会话):
|
|
88
|
+
|
|
89
|
+
| 项 | 实测 |
|
|
90
|
+
|----|------|
|
|
91
|
+
| 平台运行上下文快照 | 2 条,1133 B/条,共 2.3 KB |
|
|
92
|
+
| 真实用户消息 | 10 条,396 B/条 |
|
|
93
|
+
| 旧做法(每轮一条消息) | 10 条 × 339 B ≈ 3.4 KB |
|
|
94
|
+
| 本做法增量 | 0 条额外事件,日期约 46 B 并入已有快照 |
|
|
95
|
+
|
|
96
|
+
## 配置
|
|
97
|
+
|
|
98
|
+
`cordis.patch.yml` 里随行下发,改完需重启:
|
|
99
|
+
|
|
100
|
+
```yaml
|
|
101
|
+
- insert:
|
|
102
|
+
- id: date-wrapper
|
|
103
|
+
name: dsh-date-wrapper
|
|
104
|
+
config:
|
|
105
|
+
timeZone: Asia/Shanghai # IANA 时区;缺省用进程时区
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `timeZone` 非法会在启动时直接抛错(**不**静默降级成 UTC)。
|
|
109
|
+
- 文本里的时区名就是解析后的 IANA 名(`timeZone` 缺省时取进程时区名)。
|
|
110
|
+
- 运行上下文条目的名字是 `date-wrapper:date`,排序位 `116`(已占用:110 sandbox、115 approval、120 subagent)。
|
|
111
|
+
- 本插件**不导出 schemastery `Config`**,所以配置不走宿主的 schema 校验,校验全部在 `validateConfig()` 里手写(这也是「设置 → 插件」页没有本插件配置表单的原因)。
|
|
112
|
+
|
|
113
|
+
## 开关:靠插件激活,没有面板开关
|
|
114
|
+
|
|
115
|
+
本插件**不提供**设置面板开关,也**没有** `enabled` 之类的 config 字段。原因:
|
|
116
|
+
|
|
117
|
+
- 功能开关 = **插件行是否激活**。插件未激活 → `apply()` 不跑 → 运行上下文条目不存在 → 一个字都不会注入。
|
|
118
|
+
- 本插件没有 client 半(无 `dsh.client`),界面上没有任何属于它的控件。
|
|
119
|
+
- DSH 自带的 **设置 → 插件** 页面已经会显示每个条目的 `已启用 / 已停用`(只读,不能点)。
|
|
120
|
+
|
|
121
|
+
### 怎么关
|
|
122
|
+
|
|
123
|
+
在**你自己的** profile patch 层里按 `id` 覆盖即可 —— `C:\Users\<你>\.dsh\profiles\web\cordis.patch.yml`:
|
|
124
|
+
|
|
125
|
+
```yaml
|
|
126
|
+
- id: date-wrapper
|
|
127
|
+
disabled: true # 停用;改回 false 即恢复
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **热生效,无需重启**:该文件被 Cordis HMR 监听,`disabled: true` 会直接 `dispose` 该行的 fiber。
|
|
131
|
+
- 若 `date-wrapper` 行还不存在(未安装),这条 patch 只会打一条 `entry "date-wrapper" not found` 警告,不会让启动失败。
|
|
132
|
+
- ⚠️ 该文件必须是**顶层 YAML 数组**;写坏了会**启动失败**(DSH 对用户 patch 层是 fail-loud)。
|
|
133
|
+
|
|
134
|
+
### 怎么彻底移除
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
dsh plugin --profile web remove dsh-date-wrapper
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
卸载走 bundle 层,**需要重启** dsh web 才生效(bundle patch 不热重载)。
|
|
141
|
+
|
|
142
|
+
## 安装
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
dsh plugin --profile web add github:drscrewdriver/dsh-date-wrapper
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
重启 dsh web 并刷新页面。本地路径 / 软链安装与排错详见 [INSTALL.zh.md](./INSTALL.zh.md)。
|
|
149
|
+
|
|
150
|
+
## 验证
|
|
151
|
+
|
|
152
|
+
| # | 怎么验证 | 期望 |
|
|
153
|
+
|---|----------|------|
|
|
154
|
+
| A1 | 新开一个会话,发一句话 | 运行上下文快照里出现 `Current date: YYYY-MM-DD <时区> <星期>`(会话里显示为一条注入上下文行,来源含 `system-prompt`) |
|
|
155
|
+
| A2 | 看该行文本 | ≤50 字符(实测 46;PRD 原阈值 30,因用户指定格式放宽) |
|
|
156
|
+
| A3 | 停用插件(profile patch 置 `disabled: true`) | 后续会话快照里不再出现该行 |
|
|
157
|
+
| A4 | 搜索会话日志 | 没有 `Time sampled` / `Elapsed since` / `Browser time zone` |
|
|
158
|
+
| A5 | 把 `timeZone` 改成 `UTC` 并重启 | 日期按 UTC 计算(跨时区边界会差一天) |
|
|
159
|
+
|
|
160
|
+
## 实现要点
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
dsh-date-wrapper/
|
|
164
|
+
├── package.json # name / type: module / main / exports["."] / dsh.bundle.patch / files
|
|
165
|
+
├── cordis.patch.yml # 一行 insert(无 patch 级 id → 落在 profile 根 = 宿主面)
|
|
166
|
+
├── src/
|
|
167
|
+
│ ├── format.js # 纯函数:resolveZone / renderDate / createDateContextText / validateConfig / TEXT_LABEL
|
|
168
|
+
│ └── index.js # apply(ctx, config) → ctx.inject(['systemPrompt'], …) → systemPrompt.context(...)
|
|
169
|
+
└── tests/
|
|
170
|
+
├── format.test.mjs # 11 项(时区投影、星期、格式与长度、降级、配置校验)
|
|
171
|
+
└── context.test.mjs # 7 项(伪 ctx 断言注册契约)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- **宿主面行**:`ctx.inject(['systemPrompt'], …)` 建立子 fiber;服务缺失时静默不注册,而不是让整个 boot 失败。
|
|
175
|
+
- **fail-soft 的文本 provider**:prompt 组装期抛错会让**每一次请求**都失败,所以渲染失败时返回空串(平台会过滤掉空文本)。
|
|
176
|
+
- **不设 `complete`**:设了会顶掉整份系统提示词。
|
|
177
|
+
- **去重交给平台**:不维护任何 per-agent 状态,跨天时快照自动带上新日期。
|
|
178
|
+
- **生命周期**:注册归属 `ctx.inject` 的子 fiber,插件停用时随 fiber 回收。
|
|
179
|
+
|
|
180
|
+
## 开发:TDD + lint
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npm install # 只装 devDependencies(eslint / @eslint/js),运行时零依赖
|
|
184
|
+
|
|
185
|
+
npm run tdd # 监听模式:改 src/ 或 tests/ 自动重跑(node --test --watch)
|
|
186
|
+
npm test # 单次全量:node --test "tests/*.test.mjs"
|
|
187
|
+
node tests/format.test.mjs # 单文件直接跑(沙箱里最稳,不派生子进程)
|
|
188
|
+
|
|
189
|
+
npm run lint # eslint .(src + tests + eslint.config.mjs)
|
|
190
|
+
npm run lint:fix # 自动修可修的
|
|
191
|
+
npm run verify # lint + test,提交前跑这一条
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 红-绿-重构
|
|
195
|
+
|
|
196
|
+
测试用例直接对应验收项,流程是「先写一条会红的断言,再让它变绿」:
|
|
197
|
+
|
|
198
|
+
| 步骤 | 动作 | 命令 |
|
|
199
|
+
|------|------|------|
|
|
200
|
+
| 1 红 | 在 `tests/*.test.mjs` 里写一条按验收项命名的断言,断言当前行为**不满足**的期望 | `npm run tdd` |
|
|
201
|
+
| 2 绿 | 在 `src/` 里写最小实现让它通过,不动其它断言 | `npm run tdd` |
|
|
202
|
+
| 3 重构 | 保持全绿的前提下整理命名/抽纯函数;`src/format.js` 承担全部纯逻辑,`src/index.js` 只做注册 | `npm run tdd` |
|
|
203
|
+
| 4 闸门 | 提交前跑 lint + 全量测试 | `npm run verify` |
|
|
204
|
+
|
|
205
|
+
现有 18 条断言:`format.test.mjs`(11 条)覆盖纯函数,`context.test.mjs`(7 条)用伪 ctx 断言注册契约。
|
|
206
|
+
|
|
207
|
+
### lint 配置要点
|
|
208
|
+
|
|
209
|
+
- ESLint 10 扁平配置(`eslint.config.mjs`),`@eslint/js` recommended 为基线。
|
|
210
|
+
- 收紧项:`eqeqeq`、`prefer-const`、`object-shorthand`、`no-unused-vars`(`_` 前缀豁免)。
|
|
211
|
+
- 显式声明 Node 全局 `crypto` / `console` / `process`,否则 `no-undef` 会误报。
|
|
212
|
+
|
|
213
|
+
## 已知限制
|
|
214
|
+
|
|
215
|
+
- **fixed-prompt preset 下不生效**:若某个 preset 的 persona 设了 `includeRuntimeContext: false`(官方 `minimal` 与本地 `simple-reply` 都是),`assemble()` 会返回 `contexts: []`,本插件的条目会被整段丢掉。这类 preset 的设计意图就是「不允许后续 listener 往提示词里加东西」。
|
|
216
|
+
- **旧快照会留在历史里**:日期变化时平台追加一条新快照(旧快照保留),靠快照自带的 "This snapshot supersedes earlier runtime-context snapshots" 声明让最新一条生效 —— 这与平台处理 cwd / sandbox / approval 策略变化的方式一致。
|
|
217
|
+
- **bundle patch 不热重载**:改 `cordis.patch.yml` 或升级插件后必须重启 dsh web(改 profile patch 的 `disabled` 是热生效的)。
|
|
218
|
+
- **不加载也不过滤 `dsh-time-context`**:若你在某个 preset 里显式挂载它,它的 verbose 文本会照常出现。不要同时使用。
|
|
219
|
+
- **契约点未做运行时探测**:`systemPrompt.context` 目前是裸调用,若 DSH 未来改名,表现为插件加载失败而非静默降级(待办见 `HANDOVER.md` §7)。
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|