dsh-composer-ease 0.0.0-stage → 0.9.66
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 +180 -0
- package/LICENSE +21 -0
- package/README.i18n.yaml +30 -0
- package/README.md +79 -2
- package/README.zh.md +80 -0
- package/THIRD-PARTY.md +259 -0
- package/cordis.patch.yml +11 -0
- package/lib/cjk-domain.js +24 -0
- package/lib/cjk-lexicon.js +30049 -0
- package/lib/client/10-foundation.js +396 -0
- package/lib/client/20-pairing.js +177 -0
- package/lib/client/30-popup.js +320 -0
- package/lib/client/40-language.js +174 -0
- package/lib/client/45-settings.js +225 -0
- package/lib/client/50-caret.js +254 -0
- package/lib/client/60-keys.js +242 -0
- package/lib/client/70-input.js +369 -0
- package/lib/client/80-apply.js +76 -0
- package/lib/client/_head.js +17 -0
- package/lib/client/_tail.js +3 -0
- package/lib/client.js +2262 -0
- package/lib/domain-words.js +13 -0
- package/lib/en-common.js +54 -0
- package/lib/en-lexicon.js +66 -0
- package/lib/fuzzy.js +485 -0
- package/lib/index.js +438 -0
- package/lib/words.js +88 -0
- package/package.json +63 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// dsh-composer-ease —— 服务端半边:给浏览器侧补全弹层供数据。
|
|
2
|
+
// 两个只读路由(本机同源):
|
|
3
|
+
// GET /dsh-composer-ease/config.json → 键位/配对表 + 词表规模与**借用出处**(诊断用)
|
|
4
|
+
// GET /dsh-composer-ease/words?prefix=&lang= → 候选:语言词表(标签/关键字/属性名)
|
|
5
|
+
// + 工作区文件路径 + settings 里配置的模型名 + 常用代码片段
|
|
6
|
+
// 数据来源:词表见 lib/words.js(只借开源数据、出处与许可证在文件头与 THIRD-PARTY.md);
|
|
7
|
+
// 其余都是磁盘现读。不发消息、不写任何文件。
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import os from 'node:os'
|
|
11
|
+
import { LANG_WORDS, HTML_ATTRS, LANG_ALIASES, WORD_STATS, COMMON_WORDS } from './words.js'
|
|
12
|
+
import { fuzzyScore } from './fuzzy.js'
|
|
13
|
+
import { CJK_LEXICON, lexiconMatches, isCompleteWord, CJK_MAX_LEN } from './cjk-lexicon.js'
|
|
14
|
+
import { enMatches, isCompleteEnWord, EN_COMMON } from './en-lexicon.js'
|
|
15
|
+
import { DOMAIN_WORDS } from './domain-words.js'
|
|
16
|
+
|
|
17
|
+
const name = 'dsh-composer-ease'
|
|
18
|
+
const inject = ['webServer']
|
|
19
|
+
|
|
20
|
+
/** 借用的数据来源(写清源地址与 commit;只借数据,不搬代码)。 */
|
|
21
|
+
const SOURCES = [
|
|
22
|
+
{ what: 'JS / Python / Java 关键字', repo: 'https://github.com/microsoft/monaco-editor', license: 'MIT', commit: 'f54d769', path: 'src/languages/definitions/<lang>/<lang>.ts' },
|
|
23
|
+
{ what: 'HTML 标签与全局属性', repo: 'https://github.com/codemirror/lang-html', license: 'MIT', commit: '06eec80', path: 'src/complete.ts' },
|
|
24
|
+
{ what: 'CSS 伪类与 at 规则表', repo: 'https://github.com/codemirror/lang-css', license: 'MIT', commit: '7b7835b', path: 'src/complete.ts' },
|
|
25
|
+
{ what: 'CSS 属性名', repo: 'https://github.com/mdn/browser-compat-data', license: 'CC0-1.0', commit: '0416078', path: 'css/properties/*.json' },
|
|
26
|
+
// 中文词典与英文词表都是「本仓自带数据」,出处写自家仓库;中文数据的上游另在 cjk 那行注明。
|
|
27
|
+
{ what: '中文常用词表(2-4 字,按频次前 30000)', repo: 'https://github.com/mengge237/dsh-composer-ease', license: 'MIT', commit: '02e67e6', path: 'lib/cjk-lexicon.js(上游 fxsjy/jieba extra_dict/dict.txt.big,MIT)' },
|
|
28
|
+
{ what: '英文词表(T2:30k,语料频次排序)', repo: 'https://github.com/words/an-array-of-english-words', license: 'MIT', commit: 'v2.0.0', path: 'lib/en-lexicon.js(生成:tools/gen-lexicon.mjs)' },
|
|
29
|
+
{ what: '本机领域词(T3:文档高频、词表没有的)', repo: 'https://github.com/mengge237/dsh-composer-ease', license: 'MIT', commit: 'e00dda0', path: 'lib/domain-words.js' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
33
|
+
|
|
34
|
+
/** VSCode 风格的自动配对表(行为参照 VSCode,MIT 项目的交互设计,未复制源码)。
|
|
35
|
+
* 补全角/中文成对符:中文输入法下原来「(」不会有「)」。 */
|
|
36
|
+
const PAIRS = { '(': ')', '[': ']', '{': '}', '`': '`', '"': '"', "'": "'",
|
|
37
|
+
'(': ')', '【': '】', '「': '」', '《': '》', '“': '”', '‘': '’' }
|
|
38
|
+
|
|
39
|
+
/** 常用代码片段(kind:'snippet',accept 时整段插入)。 */
|
|
40
|
+
// 片段支持 VS Code 的占位符语法 `${n:默认值}`:接受后光标落在第 1 个占位符,Tab 依次往后跳
|
|
41
|
+
// (补;客户端 expandSnippet 负责展开与算落点)。
|
|
42
|
+
const SNIPPETS = [
|
|
43
|
+
// **单行**片段:实测,多行文本在这台宿主上插不可靠 —— Lexical 的模型选区
|
|
44
|
+
// 不认 DOM Range,粘贴时选区已塌到末尾(结果变成"追加"而不是"替换 token")。
|
|
45
|
+
// 所以片段一律写成一行;需要占位符就用 VS Code 的 `${n:默认值}` 语法,客户端 expandSnippet 展开。
|
|
46
|
+
{ label: 'function', value: 'function ${1:name}(${2:args}) {}' },
|
|
47
|
+
{ label: 'arrow', value: 'const ${1:fn} = (${2:x}) => ${3:body}' },
|
|
48
|
+
{ label: 'if', value: 'if (${1:cond}) {}' },
|
|
49
|
+
{ label: 'for', value: 'for (let ${1:i} = 0; ${1:i} < ${2:n}; ${1:i}++) {}' },
|
|
50
|
+
{ label: 'try/catch', value: 'try {} catch (${1:err}) {}' },
|
|
51
|
+
{ label: 'log', value: 'console.log(${1:value})' },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
function readJsonOrNull(p) {
|
|
55
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')) } catch { return null }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 工作区根:宿主 workspace.json(v2 形状:tables.workspaces 是 id→{path,...} 字典),存在才算,最多取 3 个。 */
|
|
59
|
+
/* 最近动过的那个工作区就是"当前":workspace.json 每条都有 updatedAt,
|
|
60
|
+
* 按它倒序取前 3 个。第一个会被当作 current(见 filesCached 的 bonus)——
|
|
61
|
+
* 否则打 README 的前几个字母时,排上来的是别的项目的 README。 */
|
|
62
|
+
function workspaceRoots() {
|
|
63
|
+
const st = readJsonOrNull(path.join(DSH_HOME, 'storages', 'workspace.json'))
|
|
64
|
+
const tab = st && st.tables && st.tables.workspaces
|
|
65
|
+
const list = tab ? Object.values(tab) : (st && Array.isArray(st.workspaces) ? st.workspaces : [])
|
|
66
|
+
const withTime = []
|
|
67
|
+
for (const w of list) {
|
|
68
|
+
const p = w && w.path
|
|
69
|
+
if (!p) continue
|
|
70
|
+
let ok = false
|
|
71
|
+
try { ok = fs.statSync(p).isDirectory() } catch {}
|
|
72
|
+
if (!ok) continue
|
|
73
|
+
const t = Date.parse((w && w.updatedAt) || (w && w.createdAt) || '') || 0
|
|
74
|
+
withTime.push({ p, t })
|
|
75
|
+
}
|
|
76
|
+
withTime.sort((a, b) => b.t - a.t)
|
|
77
|
+
return withTime.slice(0, 3).map((x) => x.p)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* 不该出现在候选里的目录:构建产物之外,还包括本机约定的中间态目录
|
|
81
|
+
* (临时目录/临时目录/3_产出 之类)—— :输入框里常常要用 README 当上下文,
|
|
82
|
+
* 可候选里冒出来的却是 临时目录 下的东西,而 400 条上限先被它们占满。 */
|
|
83
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'target', '__pycache__', '.next',
|
|
84
|
+
'.venv', 'venv', '.idea', '.vscode', '临时目录', '临时目录', 'coverage', 'out', 'vendor', '.cache',
|
|
85
|
+
'.turbo', 'backups', 'backups-live', 'storages', 'sessions', '.dsh-build', '.sidebar-upstream',
|
|
86
|
+
// 别人的仓副本:里面的 README 不是"我的上下文",直接跳过(contextScore 里那 -500 只算第二道保险)
|
|
87
|
+
'ref_repo', 'third_party', 'external', '_upstream'])
|
|
88
|
+
|
|
89
|
+
/* 「常用作上下文」的打分:越大越靠前。README / CHANGELOG / AGENTS 这类文件是输入框里
|
|
90
|
+
* 最常被引用的,论"值不值得挤进前 400 条"它们该在构建产物与笔记之上。 */
|
|
91
|
+
const TOP_NAMES = /^(readme|changelog|agents|claude|contributing|package)\.md$/i
|
|
92
|
+
/* 别人的仓副本(ref_repo / third_party / external / _upstream)里的 README 不是"我的上下文",
|
|
93
|
+
* 扣掉 —— 否则它们会顶掉当前工作区里真正在用的那份。 */
|
|
94
|
+
const VENDORED = /(^|\/)(ref_repo|third_party|external|_upstream|vendor)\//i
|
|
95
|
+
// 一次反馈。
|
|
96
|
+
// 判据是**路径**而不是仓库名:harness 安装目录、插件目录、依赖目录一律压到底。
|
|
97
|
+
const HARNESS_DIR = /(deepseek[-_]harness|dsh-(fixes|kit|shim|020rc2|017)|node_modules|(^|\/)plugins\/)/i
|
|
98
|
+
function contextScore(rel, depth, bonus) {
|
|
99
|
+
const base = rel.slice(rel.lastIndexOf('/') + 1)
|
|
100
|
+
let s = 0
|
|
101
|
+
if (TOP_NAMES.test(base) && depth === 0) s = 1000
|
|
102
|
+
else if (TOP_NAMES.test(base)) s = 700
|
|
103
|
+
else if (rel.endsWith('.md') && depth === 0) s = 500
|
|
104
|
+
else if (rel.startsWith('docs/') && rel.endsWith('.md')) s = 400
|
|
105
|
+
else if (rel.endsWith('.md')) s = 300
|
|
106
|
+
if (VENDORED.test(rel)) s -= 500
|
|
107
|
+
if (HARNESS_DIR.test(rel)) s -= 2000 // 压到 0 以下:只有词候选全部落空时它才可能进池
|
|
108
|
+
return s + (bonus || 0)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 目录内相对路径清单:深度 ≤ maxDepth,按「常用作上下文」打分降序,同分按修改时间新→旧。 */
|
|
112
|
+
function collectFiles(root, maxDepth, cap, bonus) {
|
|
113
|
+
const items = []
|
|
114
|
+
const walk = (dir, rel, depth) => {
|
|
115
|
+
if (depth > maxDepth) return
|
|
116
|
+
let entries = []
|
|
117
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
118
|
+
for (const e of entries) {
|
|
119
|
+
if (e.name.startsWith('.') && depth > 0) continue
|
|
120
|
+
const childRel = rel ? rel + '/' + e.name : e.name
|
|
121
|
+
if (e.isDirectory()) {
|
|
122
|
+
if (!SKIP_DIRS.has(e.name)) walk(path.join(dir, e.name), childRel, depth + 1)
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
let mtime = 0
|
|
126
|
+
try { mtime = fs.statSync(path.join(dir, e.name)).mtimeMs } catch {}
|
|
127
|
+
items.push({ rel: childRel, score: contextScore(childRel, depth, bonus), mtime })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
walk(root, '', 0)
|
|
131
|
+
items.sort((a, b) => (b.score - a.score) || (b.mtime - a.mtime) || (a.rel < b.rel ? -1 : 1))
|
|
132
|
+
const out = []
|
|
133
|
+
for (const it of items) {
|
|
134
|
+
if (out.length >= cap) break
|
|
135
|
+
out.push({ rel: it.rel, ctx: it.score })
|
|
136
|
+
}
|
|
137
|
+
return out
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 从 profile 用户层/settings.yaml 扒模型 id:真源里模型条目缩进 ≥ 8 的 `- id:`。 */
|
|
141
|
+
function parseModelIds(text) {
|
|
142
|
+
const out = []
|
|
143
|
+
for (const m of String(text || '').matchAll(/^ {8,}- id:\s*([A-Za-z0-9._:/-]+)/gm)) {
|
|
144
|
+
if (out.indexOf(m[1]) < 0) out.push(m[1])
|
|
145
|
+
}
|
|
146
|
+
return out
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function modelIdsAllProfiles() {
|
|
150
|
+
const ids = []
|
|
151
|
+
for (const p of [path.join(DSH_HOME, 'settings.yaml'),
|
|
152
|
+
path.join(DSH_HOME, 'profiles', 'web', 'cordis.patch.yml'),
|
|
153
|
+
path.join(DSH_HOME, 'profiles', 'desktop', 'cordis.patch.yml')]) {
|
|
154
|
+
try { for (const id of parseModelIds(fs.readFileSync(p, 'utf8'))) if (ids.indexOf(id) < 0) ids.push(id) } catch {}
|
|
155
|
+
}
|
|
156
|
+
return ids
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 打分排序:**用 VS Code 的 fuzzyScore**(lib/fuzzy.js 逐字取自 microsoft/vscode,MIT)。
|
|
160
|
+
* 返回的第二个以后是命中位置,客户端拿它给列表加粗 —— 与 VS Code 的建议列表一致。
|
|
161
|
+
* 文件候选另外过一道可达性闸:路径段前缀,或查询 ≥3 字符时的段内子串
|
|
162
|
+
* (路径太长,单字符查询会命中一大片,逼着人逐个核对)。 */
|
|
163
|
+
/** 中文候选:词典里更长的词(前缀命中)。**是否允许提示由 isCompleteWord 决定**——
|
|
164
|
+
* 碎片(如「档管」)连门都进不来(一次反馈要求)。 */
|
|
165
|
+
function lexiconCandidates(prefix) {
|
|
166
|
+
const p = String(prefix || '')
|
|
167
|
+
if (!p || !/[\u4e00-\u9fff]/.test(p)) return []
|
|
168
|
+
// 边界交给词典:只要词典里有以它开头的词就给,词长由词表决定(2~4 字不等),
|
|
169
|
+
// 不再用「前缀必须是完整词」或固定长度上限去卡(碎片前缀自然匹配不到任何词)。
|
|
170
|
+
return lexiconMatches(p, 8).map((w) => ({ kind: 'word', value: w, detail: '词典', src: 'cjk-lexicon.js' }))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function rankCandidates(cands, prefix, cap) {
|
|
174
|
+
const p = String(prefix || '').toLowerCase()
|
|
175
|
+
const re = p ? new RegExp('(^|[/\\\\])' + p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) : null
|
|
176
|
+
const scored = []
|
|
177
|
+
for (const c of cands) {
|
|
178
|
+
const v = String(c.value || '')
|
|
179
|
+
const vl = v.toLowerCase()
|
|
180
|
+
if (p && c.kind === 'file') {
|
|
181
|
+
if (!re.test(vl) && !(p.length >= 3 && vl.includes(p))) continue
|
|
182
|
+
}
|
|
183
|
+
if (!p) {
|
|
184
|
+
scored.push({ c, score: 0, matches: [], len: v.length })
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
// 借 VS Code 的 `suggest.matchOnWordStartOnly`(**只对长前缀的「词」开**):
|
|
188
|
+
// VS Code 默认关(模糊优先),但词候选在 ≥4 字符时还允许中间匹配,
|
|
189
|
+
// 只会让冷词与路径混排(`libr` 出 libra/libri 却不出 library 那种感觉)。
|
|
190
|
+
// 收敛成:kind=word 且 prefix ≥4 → 必须词首匹配;文件与短前缀保持模糊。
|
|
191
|
+
// 词的『必须前缀匹配』门槛从 4 降到 2:2 字已经很具体,模糊匹配只会带进 try/itemprop 这类无关词
|
|
192
|
+
// 2 字前缀必须前缀匹配(模糊在 2 字上只会带进 try/itemprop),3 字保留 VS Code 的模糊语义,>=4 早已要求前缀
|
|
193
|
+
if (c.kind === 'word' && (p.length === 2 || p.length >= 4) && !vl.startsWith(p)) continue
|
|
194
|
+
const r = fuzzyScore(p, p, 0, vl, vl, 0)
|
|
195
|
+
if (!r) continue
|
|
196
|
+
// VS Code 的命中位置是从尾部回填的(倒序),这里归一成正序,客户端直接拿来加粗
|
|
197
|
+
scored.push({ c, score: r[0], matches: r.slice(2).slice().sort((x, y) => x - y), len: v.length })
|
|
198
|
+
}
|
|
199
|
+
// 并列时先看上下文分(当前工作区 +200、README 类 1000)再看长度:
|
|
200
|
+
// 只按 fuzzyScore 排会把跨工作区的上下文信息丢掉,README 就排不上来。
|
|
201
|
+
scored.sort((a, b) => b.score - a.score
|
|
202
|
+
|| ((b.c.ctx || 0) - (a.c.ctx || 0))
|
|
203
|
+
|| a.len - b.len
|
|
204
|
+
|| (a.c.value < b.c.value ? -1 : 1))
|
|
205
|
+
// 纯字母前缀(≥3)时,**词候选整段排在文件候选前面**:fuzzyScore 给路径命中的分本就偏高,
|
|
206
|
+
// 不这样分区,打 dsh / gh- 时前几条永远是被 deepseek-harness 之类目录下的文件占掉
|
|
207
|
+
// (就是这个)。文件内部与词内部各自的相对顺序不变(稳定分区)。
|
|
208
|
+
let ordered = scored
|
|
209
|
+
// 带连字符的(gh-、dsh-)同样按词优先分区:它们是「领域词前缀」,不是路径
|
|
210
|
+
if (/^[A-Za-z][A-Za-z0-9-]{2,}$/.test(p)) {
|
|
211
|
+
// 词优先,但**留配额**:词最多 6 条、非词(文件/模型/片段)至少 2 条。
|
|
212
|
+
// 整段分区 + 上限 8 会把文件全饿死(短前缀 'hel' 上只剩 help/held/hele…,
|
|
213
|
+
// 想选文件时一条都看不到),单测直接照出来了。
|
|
214
|
+
const word = [], other = []
|
|
215
|
+
for (const s of scored) (s.c.kind === 'word' ? word : other).push(s)
|
|
216
|
+
const cap0 = cap || 8
|
|
217
|
+
const wq = Math.min(6, word.length)
|
|
218
|
+
const oq = Math.min(Math.max(2, cap0 - wq), other.length)
|
|
219
|
+
ordered = word.slice(0, wq).concat(other.slice(0, oq))
|
|
220
|
+
if (ordered.length < cap0) ordered = ordered.concat(word.slice(wq), other.slice(oq))
|
|
221
|
+
}
|
|
222
|
+
// **不做相对分数截断**(试过又撤):VS Code 的 fuzzyScore 语义是「弱匹配排在下面,
|
|
223
|
+
// 但仍要列出来」(`abc` 对 `axbxc` 是负分也照样出现),按 50% 砍会把它们删掉、并误删
|
|
224
|
+
// `libsdk/auth.ts` 这类合法路径。压噪声靠三件:上限 8、词优先分区、harness 目录降权。
|
|
225
|
+
return ordered.slice(0, cap || 12).map((s) => Object.assign({}, s.c, { matches: s.matches }))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 文件索引缓存:以前每敲一个键都全盘走一遍工作区(深处 3 层、封顶 400),纯浪费。
|
|
229
|
+
* 20s TTL;同时缓存模型 id(要重读 settings 真源)。 */
|
|
230
|
+
const INDEX_TTL_MS = 20000
|
|
231
|
+
const indexCache = { files: null, filesAt: 0, models: null, modelsAt: 0 }
|
|
232
|
+
function filesCached() {
|
|
233
|
+
const now = Date.now()
|
|
234
|
+
if (indexCache.files && now - indexCache.filesAt < INDEX_TTL_MS) return indexCache.files
|
|
235
|
+
const out = []
|
|
236
|
+
const roots = workspaceRoots()
|
|
237
|
+
for (let i = 0; i < roots.length; i++) {
|
|
238
|
+
const base = roots[i].replace(/\\/g, '/').replace(/\/+$/, '')
|
|
239
|
+
// 当前工作区(roots[0],即最近动过的那个)整体 +200,让它压过别的项目里的同名文件
|
|
240
|
+
const bonus = i === 0 ? 200 : 0
|
|
241
|
+
for (const f of collectFiles(base, 3, 400, bonus)) {
|
|
242
|
+
out.push({ rel: f.rel, base: path.basename(base), ctx: f.ctx, current: i === 0 })
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
indexCache.files = out
|
|
246
|
+
indexCache.filesAt = now
|
|
247
|
+
return out
|
|
248
|
+
}
|
|
249
|
+
function modelsCached() {
|
|
250
|
+
const now = Date.now()
|
|
251
|
+
if (indexCache.models && now - indexCache.modelsAt < INDEX_TTL_MS) return indexCache.models
|
|
252
|
+
indexCache.models = modelIdsAllProfiles()
|
|
253
|
+
indexCache.modelsAt = now
|
|
254
|
+
return indexCache.models
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 围栏标记 → 词表键;认不出就返回空串(只上公共词表)。 */
|
|
258
|
+
function normalizeLang(hint) {
|
|
259
|
+
const h = String(hint || '').trim().toLowerCase().replace(/^\./, '')
|
|
260
|
+
if (!h) return ''
|
|
261
|
+
if (Object.prototype.hasOwnProperty.call(LANG_WORDS, h)) return h
|
|
262
|
+
return LANG_ALIASES[h] || ''
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 语言词表 → 候选。公共池(HTML 标签 + JS 关键字)永远在,保证没有语言上下文时也能补(inp → input)。 */
|
|
266
|
+
function wordCandidates(prefix, lang) {
|
|
267
|
+
const out = []
|
|
268
|
+
// 中文:只由词典提供候选(词长由词表决定)。不再混入名词表池 —— 那些池没有按前缀过滤,
|
|
269
|
+
// 漏进来就是「打中文却提示 README.md」这种噪声;碎片前缀在词典里自然匹配不到词。
|
|
270
|
+
if (/^[\u4e00-\u9fff]/.test(String(prefix || ''))) return lexiconCandidates(prefix)
|
|
271
|
+
const seen = new Set()
|
|
272
|
+
const push = (value, detail, src, langKey) => {
|
|
273
|
+
if (!value || seen.has(value)) return
|
|
274
|
+
seen.add(value)
|
|
275
|
+
out.push({ kind: 'word', value, detail, src, lang: langKey })
|
|
276
|
+
}
|
|
277
|
+
const pool = []
|
|
278
|
+
if (lang && LANG_WORDS[lang]) pool.push([LANG_WORDS[lang], lang + ' 词表', lang])
|
|
279
|
+
// 高频文件名先入池:README.md / package.json / 本地配置 这类是用户提问里最常被点名的词,
|
|
280
|
+
// 不该等语言词表排完才出现(语言只是其中一个表象)。
|
|
281
|
+
pool.push([COMMON_WORDS, '常用文件名'])
|
|
282
|
+
pool.push([LANG_WORDS.html, 'html 标签', 'html'])
|
|
283
|
+
pool.push([LANG_WORDS.javascript, 'JS 关键字', 'javascript'])
|
|
284
|
+
pool.push([HTML_ATTRS, 'html 属性', 'html'])
|
|
285
|
+
if (lang === 'css') pool.push([LANG_WORDS.css, 'CSS 属性', 'css'])
|
|
286
|
+
// 来源即档位:lang 关键字 450 > 常用文件名 445 > html 标签/属性 425(都低于英文常用词 440 之外的加成)
|
|
287
|
+
const tierOf = (detail) => (/关键字/.test(detail) ? 450 : (/常用文件名/.test(detail) ? 445 : (/html/.test(detail) ? 425 : 0)))
|
|
288
|
+
for (const [words, detail, langKey] of pool) for (const w of words) { const n0 = out.length; push(w, detail, 'words.js', langKey); if (out.length > n0 && tierOf(detail)) out[out.length - 1].ctx = tierOf(detail) }
|
|
289
|
+
// 词典补在池子**之后**:words.js 里已有的词,出处仍记 words.js(去重时 `seen` 先到先得)。
|
|
290
|
+
if (/^[\u4e00-\u9fff]/.test(String(prefix || ''))) {
|
|
291
|
+
for (const w of lexiconMatches(prefix, 8)) push(w, '词典', 'cjk-lexicon.js', 'zh')
|
|
292
|
+
} else if (/^[A-Za-z][A-Za-z-]{1,23}$/.test(String(prefix || ''))) {
|
|
293
|
+
// 英文(补):中文靠「完整词义」清零,英文**不清零** —— 前缀正是补全的用法。
|
|
294
|
+
const pl = String(prefix || '').toLowerCase()
|
|
295
|
+
// 短前缀(<=3)只留常用词:`ty` 下 tye/tyde/tyed 这类生僻词是纯噪声
|
|
296
|
+
for (const w of enMatches(prefix, 8)) { if (pl.length <= 3 && !EN_COMMON.has(String(w).toLowerCase())) continue; push(w, '英文词', 'en-lexicon.js', 'en') }
|
|
297
|
+
// T3 本机领域词(dsh / gh-archive / deepseek-harness 这类):词表里没有、但本机文档高频。
|
|
298
|
+
// 单独计数 —— 早先拿 out.length 当计数器,池子早把 out 撑满,一次也没推出去。
|
|
299
|
+
let domN = 0
|
|
300
|
+
for (const w of DOMAIN_WORDS) {
|
|
301
|
+
if (domN >= 6) break
|
|
302
|
+
if (!w.startsWith(pl)) continue
|
|
303
|
+
const before = out.length
|
|
304
|
+
push(w, '本机词', 'domain-words.js', 'en')
|
|
305
|
+
if (out.length > before) domN++
|
|
306
|
+
}
|
|
307
|
+
// 统一抬分:**所有**以 prefix 开头的词候选(不论出自词表、关键字表还是领域词)按三档给 ctx,
|
|
308
|
+
// 否则 words.js 里的 input 会被词表里的 inpours 压住(它没有 ctx)。
|
|
309
|
+
for (const c of out) {
|
|
310
|
+
if (c.kind !== 'word') continue
|
|
311
|
+
const v = String(c.value || '').toLowerCase()
|
|
312
|
+
if (!v.startsWith(pl)) continue
|
|
313
|
+
// 整词加分只给「真的是一个词」的(isCompleteEnWord):否则 docum 这种非词串也会吃满分,
|
|
314
|
+
// 反过来把 document 压下去(实测)。
|
|
315
|
+
// Rare lexicon words (not in EN_COMMON, e.g. tye/tyde) sink to 380 so keywords win.
|
|
316
|
+
const rare = c.src === 'en-lexicon.js' && !EN_COMMON.has(v)
|
|
317
|
+
const base = c.ctx || 0; const want = (v === pl && isCompleteEnWord(v)) ? 480 : (EN_COMMON.has(v) ? Math.max(base, 440) : (rare ? 380 : Math.max(base, 400)))
|
|
318
|
+
c.ctx = Math.max(c.ctx || 0, want)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return out
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 词典驱动的中文分词(正向最大匹配):把草稿里的汉字串切成词。
|
|
325
|
+
* 为什么不用 n 字切片:没有分词器时切出来的块既可能是句子片段,又必须靠一个长度上限兜着;
|
|
326
|
+
* 用词表切词,边界由数据决定,长度上限自然等于词表里最长的词。 */
|
|
327
|
+
function segmentCjk(text) {
|
|
328
|
+
const out = []
|
|
329
|
+
for (const run of String(text || '').match(/[\u4e00-\u9fff]+/g) || []) {
|
|
330
|
+
let i = 0
|
|
331
|
+
while (i < run.length) {
|
|
332
|
+
let hit = ''
|
|
333
|
+
for (let n = Math.min(CJK_MAX_LEN, run.length - i); n >= 2; n--) {
|
|
334
|
+
const cand = run.slice(i, i + n)
|
|
335
|
+
if (CJK_LEXICON.has(cand)) { hit = cand; break }
|
|
336
|
+
}
|
|
337
|
+
if (hit) { out.push(hit); i += hit.length } else i += 1
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return out
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** 草稿里的中文词(VS Code 的 word based suggestions 口径:候选来自文档,不是凭空造词)。
|
|
344
|
+
* 分词用词典,所以只吐真正的词;按出现次数排,只留以 prefix 开头的。 */
|
|
345
|
+
function draftCjkWords(prefix, draft, cap) {
|
|
346
|
+
const p = String(prefix || '')
|
|
347
|
+
if (!p || !/[\u4e00-\u9fff]/.test(p)) return []
|
|
348
|
+
const counts = new Map()
|
|
349
|
+
for (const w of segmentCjk(String(draft || '').slice(-2000))) {
|
|
350
|
+
if (!w.startsWith(p)) continue
|
|
351
|
+
counts.set(w, (counts.get(w) || 0) + 1)
|
|
352
|
+
}
|
|
353
|
+
return [...counts.entries()]
|
|
354
|
+
.sort((a, b) => b[1] - a[1] || a[0].length - b[0].length)
|
|
355
|
+
.slice(0, cap || 6)
|
|
356
|
+
.map(([w]) => ({ kind: 'word', value: w, detail: '本文', src: 'draft' }))
|
|
357
|
+
}
|
|
358
|
+
function buildCandidates(prefix, lang, draft) {
|
|
359
|
+
// 中文不再按「前缀是不是完整词」整条清零:要不要提示由**词典有没有以它开头的词**决定
|
|
360
|
+
// (见 lexiconCandidates / 路由里的 wordGate),碎片前缀自然匹配不到词。
|
|
361
|
+
const cands = []
|
|
362
|
+
for (const c of draftCjkWords(prefix, draft, 6)) cands.push(c)
|
|
363
|
+
for (const c of wordCandidates(prefix, lang)) cands.push(c)
|
|
364
|
+
for (const f of filesCached()) cands.push({ kind: 'file', value: f.rel, detail: f.base, ctx: f.ctx })
|
|
365
|
+
for (const id of modelsCached()) cands.push({ kind: 'model', value: id, detail: '模型' })
|
|
366
|
+
for (const s of SNIPPETS) cands.push({ kind: 'snippet', value: s.value, detail: '片段 ' + s.label, label: s.label })
|
|
367
|
+
// 同一个词可能既来自草稿又来自词典(detail 不同):只留先出现的那条,草稿词优先
|
|
368
|
+
const seenVal = new Set()
|
|
369
|
+
const uniq = cands.filter((c) => { const v = String(c.value); if (seenVal.has(v)) return false; seenVal.add(v); return true })
|
|
370
|
+
return rankCandidates(uniq, prefix, 8)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** 信任栅栏:只接本机(loopback Host;带 Origin 必须同源)。抄 dsh-better-sidebar/whale 同一套判据。 */
|
|
374
|
+
function isTrusted(ctx, req) {
|
|
375
|
+
const host = String(req.headers.host || '')
|
|
376
|
+
const h = host.split(':')[0]
|
|
377
|
+
if (!(h === '127.0.0.1' || h === 'localhost' || h === '[::1]')) return false
|
|
378
|
+
const origin = req.headers.origin
|
|
379
|
+
if (origin) { try { if (new URL(origin).host !== host) return false } catch { return false } }
|
|
380
|
+
return true
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function send(res, code, obj) {
|
|
384
|
+
const body = JSON.stringify(obj)
|
|
385
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
386
|
+
res.end(body)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function apply(ctx) {
|
|
390
|
+
const disposers = []
|
|
391
|
+
const registerRoute = (route) => ctx.webServer.register(Object.assign({}, route, {
|
|
392
|
+
handler: async (req, res) => {
|
|
393
|
+
if (!isTrusted(ctx, req)) return send(res, 403, { ok: false, error: '只接受本机来源' })
|
|
394
|
+
return route.handler(req, res)
|
|
395
|
+
},
|
|
396
|
+
}))
|
|
397
|
+
|
|
398
|
+
disposers.push(registerRoute({
|
|
399
|
+
kind: 'exact',
|
|
400
|
+
path: '/' + name + '/config.json',
|
|
401
|
+
handler(req, res) {
|
|
402
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return send(res, 405, { ok: false, error: 'use GET' })
|
|
403
|
+
send(res, 200, {
|
|
404
|
+
ok: true, pairs: PAIRS,
|
|
405
|
+
keys: { completion: 'Ctrl+Space(打 2 个以上字母也自动弹)', accept: 'Tab / Enter', dismiss: 'Esc' },
|
|
406
|
+
version: '0.2.0',
|
|
407
|
+
wordStats: WORD_STATS,
|
|
408
|
+
sources: SOURCES,
|
|
409
|
+
})
|
|
410
|
+
},
|
|
411
|
+
}))
|
|
412
|
+
|
|
413
|
+
disposers.push(registerRoute({
|
|
414
|
+
kind: 'exact',
|
|
415
|
+
path: '/' + name + '/words',
|
|
416
|
+
handler(req, res) {
|
|
417
|
+
try {
|
|
418
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return send(res, 405, { ok: false, error: 'use GET' })
|
|
419
|
+
const url = new URL(req.url, 'http://127.0.0.1')
|
|
420
|
+
const prefix = url.searchParams.get('prefix') || ''
|
|
421
|
+
// 草稿尾巴:中文候选按 VS Code 的口径从文档里取词,最多带 800 字
|
|
422
|
+
const draft = String(url.searchParams.get('draft') || '').slice(-800)
|
|
423
|
+
const lang = normalizeLang(url.searchParams.get('lang'))
|
|
424
|
+
send(res, 200, { ok: true, prefix, lang, candidates: buildCandidates(prefix, lang, draft),
|
|
425
|
+
// 这条闸给客户端用:中文前缀只有在词典里能接出词时才放行,碎片前缀一律收起补全。
|
|
426
|
+
wordGate: /^[\u4e00-\u9fff]/.test(String(prefix || '')) ? lexiconCandidates(prefix).length > 0 : true })
|
|
427
|
+
} catch (err) {
|
|
428
|
+
send(res, 500, { ok: false, error: String((err && err.message) || err) })
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
}))
|
|
432
|
+
|
|
433
|
+
ctx.effect(() => () => { for (const d of disposers) { try { d() } catch {} } })
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export { name, inject, apply }
|
|
437
|
+
export const internals = {
|
|
438
|
+
isCompleteWord, lexiconCandidates, PAIRS, SNIPPETS, SOURCES, rankCandidates, contextScore, parseModelIds, collectFiles, workspaceRoots, buildCandidates, wordCandidates, normalizeLang, DSH_HOME }
|
package/lib/words.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// dsh-composer-ease —— 各语言词表(补全用)。
|
|
2
|
+
//
|
|
3
|
+
// 只借用下面这些开源项目的**数据**(关键字 / 标签 / 属性名),一行代码都没有搬;
|
|
4
|
+
// 出处钉了 commit,许可证原文见仓根 THIRD-PARTY.md:
|
|
5
|
+
// 1) microsoft/monaco-editor (MIT) @f54d769
|
|
6
|
+
// src/languages/definitions/javascript/javascript.ts → keywords
|
|
7
|
+
// src/languages/definitions/python/python.ts → keywords
|
|
8
|
+
// src/languages/definitions/java/java.ts → keywords
|
|
9
|
+
// 2) codemirror/lang-html (MIT, © Marijn Haverbeke and others) @06eec80
|
|
10
|
+
// src/complete.ts → Tags / GlobalAttrs
|
|
11
|
+
// 3) codemirror/lang-css (MIT, © Marijn Haverbeke and others) @7b7835b
|
|
12
|
+
// src/complete.ts → pseudoClasses / atRules
|
|
13
|
+
// 4) mdn/browser-compat-data (CC0-1.0) @0416078
|
|
14
|
+
// css/properties/*.json 的文件名(标准属性;-moz-/-webkit- 前缀项已剔除)
|
|
15
|
+
//
|
|
16
|
+
// 语言别名(`js`/`ts`/`py` 这类围栏标记)在 LANG_ALIASES 里归一到键名。
|
|
17
|
+
|
|
18
|
+
export const LANG_WORDS = {
|
|
19
|
+
javascript: [
|
|
20
|
+
"async", "await", "break", "case", "catch", "class", "const", "constructor", "continue", "debugger", "default", "delete", "do", "else", "export", "extends", "false", "finally", "for", "from", "function", "get", "if", "import", "in", "instanceof", "let", "new", "null", "of", "return", "set", "static", "super", "switch", "symbol", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "while", "with", "yield",
|
|
21
|
+
],
|
|
22
|
+
python: [
|
|
23
|
+
"False", "None", "True", "_", "__bases__", "__class__", "__dict__", "__import__", "__init__", "__members__", "__methods__", "__mro__", "__name__", "__subclasses__", "abs", "all", "and", "any", "apply", "as", "assert", "async", "await", "basestring", "bin", "bool", "break", "buffer", "bytearray", "callable", "case", "chr", "class", "classmethod", "cmp", "coerce", "compile", "complex", "continue", "def", "del", "delattr", "dict", "dir", "divmod", "elif", "else", "enumerate", "eval", "except", "exec", "execfile", "file", "filter", "finally", "float", "for", "format", "from", "frozenset", "getattr", "global", "globals", "hasattr", "hash", "help", "hex", "id", "if", "import", "in", "input", "int", "intern", "is", "isinstance", "issubclass", "iter", "lambda", "len", "list", "locals", "long", "map", "match", "max", "memoryview", "min", "next", "nonlocal", "not", "object", "oct", "open", "or", "ord", "pass", "pow",
|
|
24
|
+
"print", "property", "raise", "range", "raw_input", "reduce", "reload", "repr", "return", "reversed", "round", "self", "set", "setattr", "slice", "sorted", "staticmethod", "str", "sum", "super", "try", "tuple", "type", "unichr", "unicode", "vars", "while", "with", "xrange", "yield", "zip",
|
|
25
|
+
],
|
|
26
|
+
java: [
|
|
27
|
+
"abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "false", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "permits", "private", "protected", "public", "record", "return", "sealed", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "void", "volatile", "while", "yield",
|
|
28
|
+
],
|
|
29
|
+
html: [
|
|
30
|
+
"a", "abbr", "address", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "command", "data", "datagrid", "datalist", "dd", "del", "details", "dfn", "div", "dl", "dt", "em", "embed", "eventsource", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "map", "mark", "menu", "meta", "meter", "nav", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "samp", "script", "section", "select", "slot", "small", "source", "span", "strong", "style", "sub", "summary", "sup", "table", "tbody",
|
|
31
|
+
"td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "ul", "var", "video", "wbr",
|
|
32
|
+
],
|
|
33
|
+
css: [
|
|
34
|
+
"accent-color", "align-content", "align-items", "align-self", "alignment-baseline", "all", "alt", "anchor-name", "anchor-scope", "animation", "animation-composition", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode", "animation-iteration-count", "animation-name", "animation-play-state", "animation-range", "animation-range-end", "animation-range-start", "animation-timeline", "animation-timing-function", "animation-trigger", "appearance", "aspect-ratio", "backdrop-filter", "backface-visibility", "background", "background-attachment", "background-blend-mode", "background-clip", "background-color", "background-image", "background-origin", "background-position", "background-position-x", "background-position-y", "background-repeat", "background-repeat-x", "background-repeat-y", "background-size", "baseline-shift", "baseline-source", "block-ellipsis", "block-size", "border", "border-block", "border-block-color", "border-block-end", "border-block-end-color", "border-block-end-style", "border-block-end-width", "border-block-start", "border-block-start-color", "border-block-start-style", "border-block-start-width", "border-block-style", "border-block-width", "border-bottom", "border-bottom-color", "border-bottom-left-radius", "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", "border-collapse", "border-color", "border-end-end-radius", "border-end-start-radius", "border-image", "border-image-outset", "border-image-repeat", "border-image-slice", "border-image-source", "border-image-width", "border-inline", "border-inline-color", "border-inline-end", "border-inline-end-color", "border-inline-end-style", "border-inline-end-width", "border-inline-start", "border-inline-start-color", "border-inline-start-style", "border-inline-start-width", "border-inline-style", "border-inline-width", "border-left", "border-left-color", "border-left-style", "border-left-width", "border-radius", "border-right", "border-right-color", "border-right-style", "border-right-width", "border-shape", "border-spacing",
|
|
35
|
+
"border-start-end-radius", "border-start-start-radius", "border-style", "border-top", "border-top-color", "border-top-left-radius", "border-top-right-radius", "border-top-style", "border-top-width", "border-width", "bottom", "box-align", "box-decoration-break", "box-direction", "box-flex", "box-flex-group", "box-lines", "box-ordinal-group", "box-orient", "box-pack", "box-shadow", "box-sizing", "break-after", "break-before", "break-inside", "buffered-rendering", "caption-side", "caret-animation", "caret-color", "caret-shape", "clear", "clip", "clip-path", "clip-rule", "color", "color-adjust", "color-interpolation", "color-interpolation-filters", "color-rendering", "color-scheme", "column-count", "column-fill", "column-gap", "column-height", "column-rule", "column-rule-break", "column-rule-color", "column-rule-inset", "column-rule-inset-cap", "column-rule-inset-cap-end", "column-rule-inset-cap-start", "column-rule-inset-end", "column-rule-inset-junction", "column-rule-inset-junction-end", "column-rule-inset-junction-start", "column-rule-inset-start", "column-rule-style", "column-rule-visibility-items", "column-rule-width", "column-span", "column-width", "column-wrap", "columns", "contain", "contain-intrinsic-block-size", "contain-intrinsic-height", "contain-intrinsic-inline-size", "contain-intrinsic-size", "contain-intrinsic-width", "container", "container-name", "container-type", "content", "content-visibility", "continue", "corner-block-end-shape", "corner-block-start-shape", "corner-bottom-left-shape", "corner-bottom-right-shape", "corner-bottom-shape", "corner-end-end-shape", "corner-end-start-shape", "corner-inline-end-shape", "corner-inline-start-shape", "corner-left-shape", "corner-right-shape", "corner-shape", "corner-start-end-shape", "corner-start-start-shape", "corner-top-left-shape", "corner-top-right-shape", "corner-top-shape", "counter-increment", "counter-reset", "counter-set", "cursor", "custom-property", "cx",
|
|
36
|
+
"cy", "d", "direction", "display", "dominant-baseline", "dynamic-range-limit", "empty-cells", "field-sizing", "fill", "fill-opacity", "fill-rule", "filter", "fit-tolerance", "flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow", "flex-line-count", "flex-shrink", "flex-wrap", "float", "flood-color", "flood-opacity", "font", "font-family", "font-feature-settings", "font-kerning", "font-language-override", "font-optical-sizing", "font-palette", "font-size", "font-size-adjust", "font-smooth", "font-stretch", "font-style", "font-synthesis", "font-synthesis-position", "font-synthesis-small-caps", "font-synthesis-style", "font-synthesis-weight", "font-variant", "font-variant-alternates", "font-variant-caps", "font-variant-east-asian", "font-variant-emoji", "font-variant-ligatures", "font-variant-numeric", "font-variant-position", "font-variation-settings", "font-weight", "font-width", "forced-color-adjust", "frame-sizing", "gap", "glyph-orientation-vertical", "grid", "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows", "grid-column", "grid-column-end", "grid-column-start", "grid-row", "grid-row-end", "grid-row-start", "grid-template", "grid-template-areas", "grid-template-columns", "grid-template-rows", "hanging-punctuation", "height", "hyphenate-character", "hyphenate-limit-chars", "hyphenate-limit-lines", "hyphens", "image-orientation", "image-rendering", "ime-mode", "initial-letter", "inline-size", "inset", "inset-block", "inset-block-end", "inset-block-start", "inset-inline", "inset-inline-end", "inset-inline-start", "interactivity", "interest-delay", "interest-delay-end", "interest-delay-start", "interpolate-size", "isolation", "justify-content", "justify-items", "justify-self",
|
|
37
|
+
"left", "letter-spacing", "lighting-color", "line-break", "line-clamp", "line-height", "link-parameters", "list-style", "list-style-image", "list-style-position", "list-style-type", "margin", "margin-block", "margin-block-end", "margin-block-start", "margin-bottom", "margin-inline", "margin-inline-end", "margin-inline-start", "margin-left", "margin-right", "margin-top", "margin-trim", "marker", "marker-end", "marker-mid", "marker-start", "mask", "mask-border", "mask-border-outset", "mask-border-repeat", "mask-border-slice", "mask-border-source", "mask-border-width", "mask-clip", "mask-composite", "mask-image", "mask-mode", "mask-origin", "mask-position", "mask-repeat", "mask-size", "mask-type", "math-depth", "math-shift", "math-style", "max-block-size", "max-height", "max-inline-size", "max-lines", "max-size", "max-width", "min-block-size", "min-height", "min-inline-size", "min-size", "min-width", "mix-blend-mode", "object-fit", "object-position", "object-view-box", "offset", "offset-anchor", "offset-distance", "offset-path", "offset-position", "offset-rotate", "opacity", "order", "orphans", "outline", "outline-color", "outline-offset", "outline-style", "outline-width", "overflow", "overflow-anchor", "overflow-block", "overflow-clip-margin", "overflow-inline", "overflow-wrap", "overflow-x", "overflow-y", "overlay", "overscroll-behavior", "overscroll-behavior-block", "overscroll-behavior-inline", "overscroll-behavior-x", "overscroll-behavior-y", "padding", "padding-block", "padding-block-end", "padding-block-start", "padding-bottom", "padding-inline", "padding-inline-end", "padding-inline-start", "padding-left",
|
|
38
|
+
"padding-right", "padding-top", "page", "page-break-after", "page-break-before", "page-break-inside", "paint-order", "path-length", "perspective", "perspective-origin", "place-content", "place-items", "place-self", "pointer-events", "position", "position-anchor", "position-area", "position-try", "position-try-fallbacks", "position-try-order", "position-visibility", "print-color-adjust", "quotes", "r", "reading-flow", "reading-order", "resize", "right", "rotate", "row-gap", "row-rule", "row-rule-break", "row-rule-color", "row-rule-inset", "row-rule-inset-cap", "row-rule-inset-cap-end", "row-rule-inset-cap-start", "row-rule-inset-end", "row-rule-inset-junction", "row-rule-inset-junction-end", "row-rule-inset-junction-start", "row-rule-inset-start", "row-rule-style", "row-rule-visibility-items", "row-rule-width", "ruby-align", "ruby-overhang", "ruby-position", "rule", "rule-break", "rule-color", "rule-inset", "rule-inset-cap", "rule-inset-end", "rule-inset-junction", "rule-inset-start", "rule-overlap", "rule-style", "rule-visibility-items", "rule-width", "rx", "ry", "scale", "scroll-axis-lock", "scroll-behavior", "scroll-initial-target", "scroll-margin", "scroll-margin-block", "scroll-margin-block-end", "scroll-margin-block-start", "scroll-margin-bottom", "scroll-margin-inline", "scroll-margin-inline-end", "scroll-margin-inline-start", "scroll-margin-left", "scroll-margin-right", "scroll-margin-top", "scroll-marker-group", "scroll-padding", "scroll-padding-block", "scroll-padding-block-end", "scroll-padding-block-start", "scroll-padding-bottom", "scroll-padding-inline", "scroll-padding-inline-end", "scroll-padding-inline-start", "scroll-padding-left", "scroll-padding-right", "scroll-padding-top", "scroll-snap-align", "scroll-snap-stop", "scroll-snap-type", "scroll-target-group", "scroll-timeline", "scroll-timeline-axis", "scroll-timeline-name", "scrollbar-color", "scrollbar-gutter",
|
|
39
|
+
"scrollbar-width", "shape-image-threshold", "shape-margin", "shape-outside", "shape-rendering", "size", "speak", "speak-as", "stop-color", "stop-opacity", "stroke", "stroke-color", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "tab-size", "table-layout", "text-align", "text-align-last", "text-anchor", "text-autospace", "text-box", "text-box-edge", "text-box-trim", "text-combine-upright", "text-decoration", "text-decoration-color", "text-decoration-inset", "text-decoration-line", "text-decoration-skip", "text-decoration-skip-ink", "text-decoration-skip-spaces", "text-decoration-style", "text-decoration-thickness", "text-emphasis", "text-emphasis-color", "text-emphasis-position", "text-emphasis-style", "text-fit", "text-indent", "text-justify", "text-orientation", "text-overflow", "text-rendering", "text-shadow", "text-size-adjust", "text-spacing-trim", "text-transform", "text-underline-offset", "text-underline-position", "text-wrap", "text-wrap-mode", "text-wrap-style", "timeline-scope", "timeline-trigger", "timeline-trigger-activation-range", "timeline-trigger-activation-range-end", "timeline-trigger-activation-range-start", "timeline-trigger-active-range", "timeline-trigger-active-range-end", "timeline-trigger-active-range-start", "timeline-trigger-name", "timeline-trigger-source", "top", "touch-action", "transform", "transform-box", "transform-origin", "transform-style", "transition", "transition-behavior", "transition-delay", "transition-duration", "transition-property", "transition-timing-function", "translate", "trigger-scope", "unicode-bidi", "user-modify", "user-select", "vector-effect", "vertical-align", "view-timeline", "view-timeline-axis", "view-timeline-inset", "view-timeline-name", "view-transition-class", "view-transition-group", "view-transition-name", "view-transition-scope", "visibility", "white-space", "white-space-collapse", "white-space-trim",
|
|
40
|
+
"widows", "width", "will-change", "window-drag", "word-break", "word-spacing", "wrap-inside", "writing-mode", "x", "y", "z-index", "zoom",
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const HTML_ATTRS = [
|
|
45
|
+
"accesskey", "autocapitalize", "autocorrect", "class", "contenteditable", "contextmenu", "dir", "draggable", "dropzone", "file", "hidden", "id", "inert", "itemid", "itemprop", "itemref", "itemscope", "itemtype", "lang", "rel", "role", "spellcheck", "string", "style", "tabindex", "title", "translate",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export const CSS_PSEUDO = [
|
|
49
|
+
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
export const CSS_AT_RULES = [
|
|
53
|
+
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
/* 常用文件名/目录(使用中 2026-10-23 提:README.md 这类是用户向 AI 提问时的高频词,
|
|
57
|
+
* 语言词表只是其中一种表象)。这些不是某一门语言的语法,而是**对话里反复被点名的东西**,
|
|
58
|
+
* 所以在候选里单独成一池,排在语言池之前。 */
|
|
59
|
+
export const COMMON_WORDS = [
|
|
60
|
+
"README.md", "README", "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
|
|
61
|
+
"本地配置", "CLAUDE.md", "CHANGELOG.md", "CONTRIBUTING.md", "LICENSE", "PROJECT_MEMORY.md",
|
|
62
|
+
"tsconfig.json", "jsconfig.json", "vite.config.ts", "webpack.config.js", "rollup.config.js",
|
|
63
|
+
"requirements.txt", "pyproject.toml", "setup.py", "Pipfile", "poetry.lock",
|
|
64
|
+
"pom.xml", "build.gradle", "settings.gradle", "Cargo.toml", "Cargo.lock", "go.mod", "go.sum",
|
|
65
|
+
"Dockerfile", "docker-compose.yml", ".dockerignore", "Makefile", "CMakeLists.txt",
|
|
66
|
+
".gitignore", ".gitattributes", ".editorconfig", ".env", ".env.example", ".npmrc", ".nvmrc",
|
|
67
|
+
"index.html", "index.js", "main.js", "app.vue", "favicon.ico", "manifest.json", "robots.txt",
|
|
68
|
+
"node_modules", "dist", "build", "src", "lib", "docs", "tests", "scripts", "assets", "public",
|
|
69
|
+
".github", "workflows", "actions",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
/** 围栏标记 / 常见写法 → LANG_WORDS 的键。 */
|
|
73
|
+
export const LANG_ALIASES = {
|
|
74
|
+
js: 'javascript', jsx: 'javascript', ts: 'javascript', tsx: 'javascript', typescript: 'javascript',
|
|
75
|
+
node: 'javascript', json: 'javascript',
|
|
76
|
+
py: 'python', python3: 'python',
|
|
77
|
+
htm: 'html', xhtml: 'html', xml: 'html', vue: 'html', svg: 'html',
|
|
78
|
+
scss: 'css', less: 'css', sass: 'css',
|
|
79
|
+
java: 'java',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 词表规模自述(state 路由与 selftest 用)。 */
|
|
83
|
+
export const WORD_STATS = {
|
|
84
|
+
javascript: 47, python: 129, java: 56,
|
|
85
|
+
html: 112, css: 600,
|
|
86
|
+
common: 60,
|
|
87
|
+
htmlAttrs: 27, cssPseudo: 0, cssAtRules: 0,
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-composer-ease",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.9.66",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib",
|
|
8
|
+
"cordis.patch.yml",
|
|
9
|
+
"README.md",
|
|
10
|
+
"README.zh.md",
|
|
11
|
+
"README.i18n.yaml",
|
|
12
|
+
"THIRD-PARTY.md",
|
|
13
|
+
"CHANGELOG.md"
|
|
14
|
+
],
|
|
15
|
+
"dsh": {
|
|
16
|
+
"compatibility": {
|
|
17
|
+
"dsh": ">=0.2.0-rc.2"
|
|
18
|
+
},
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "./cordis.patch.yml"
|
|
21
|
+
},
|
|
22
|
+
"client": {
|
|
23
|
+
"platform": "web",
|
|
24
|
+
"inject": [
|
|
25
|
+
"@deepseek-ai/dsh-client-ui-input-trigger"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"default": "./lib/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./client": {
|
|
34
|
+
"default": "./lib/client.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "node tools/build-client.mjs",
|
|
40
|
+
"check:size": "node tools/check-size.mjs",
|
|
41
|
+
"check:dup": "node tools/check-dup.mjs",
|
|
42
|
+
"check:refactor": "node tools/check-refactor.mjs",
|
|
43
|
+
"check": "node tools/check-size.mjs && node tools/check-dup.mjs && node tools/check-refactor.mjs && node tools/check-release.mjs",
|
|
44
|
+
"test": "node tools/check-size.mjs && node tools/check-dup.mjs && node test/selftest.mjs && node test/server-routes.mjs && node test/client-smoke.mjs && node test/client-behavior.mjs && node test/compliance.mjs",
|
|
45
|
+
"sync:host": "node tools/sync-host.mjs",
|
|
46
|
+
"check:host": "node tools/sync-host.mjs --check",
|
|
47
|
+
"check:docs": "node tools/check-docs.mjs",
|
|
48
|
+
"check:all": "npm run check && npm run check:docs && npm run check:readme && npm run check:entry && npm run check:privacy && npm run check:prose && npm run check:copies && npm run build -- --check && npm run check:real",
|
|
49
|
+
"record:demo": "node tools/record-demo.mjs",
|
|
50
|
+
"check:copies": "node tools/copies.mjs --check",
|
|
51
|
+
"sync:copies": "node tools/copies.mjs --sync",
|
|
52
|
+
"link:host": "node tools/link-host.mjs",
|
|
53
|
+
"check:prose": "node tools/verify-comment-prose.mjs",
|
|
54
|
+
"check:privacy": "node tools/check-privacy.mjs",
|
|
55
|
+
"scrub:public": "node tools/scrub-public.mjs",
|
|
56
|
+
"check:readme": "node tools/check-readme-pairing.mjs",
|
|
57
|
+
"test:real": "node tools/real-run.mjs",
|
|
58
|
+
"check:entry": "node tools/check-entry-sync.mjs",
|
|
59
|
+
"check:real": "node tools/check-real-stale.mjs"
|
|
60
|
+
},
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=20"
|
|
64
|
+
}
|
|
65
|
+
}
|