dsh-plugin-dynamic-competition-four-images 1.0.2 → 1.2.0
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/LICENSE +21 -0
- package/README.md +85 -0
- package/client.js +441 -0
- package/cordis.patch.yml +8 -3
- package/icon.svg +13 -0
- package/index.js +317 -2
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +38 -15
- package/{reference → skills/dynamic-competition-analysis-four-images-model/reference}/four-images-model-governance.md +103 -103
- package/{references → skills/dynamic-competition-analysis-four-images-model/references}/full-spec.md +768 -768
- package/{scripts → skills/dynamic-competition-analysis-four-images-model/scripts}/dynamic_competition_skill.py +618 -618
- /package/{SKILL.md → skills/dynamic-competition-analysis-four-images-model/SKILL.md} +0 -0
- /package/{examples → skills/dynamic-competition-analysis-four-images-model/examples}/example_a.json +0 -0
- /package/{examples → skills/dynamic-competition-analysis-four-images-model/examples}/example_b.json +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RootAndSource
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 动态竞争-四象模型 Skill (dsh-plugin-dynamic-competition-four-images)
|
|
2
|
+
|
|
3
|
+
基于动态竞争理论构建的 DSH(DeepSeek Harness)插件,经过以「四象模型」为框架的改进,提供六维动态竞争评分能力。
|
|
4
|
+
|
|
5
|
+
**自 1.2.0 起:插件全局安装,插件内的技能由用户按工作区选择性添加**(设置 → 插件 → 四象模型),技能只写入被选择的工作区,不会成为全局技能。
|
|
6
|
+
|
|
7
|
+
## ✨ 核心能力
|
|
8
|
+
|
|
9
|
+
- 六维动态竞争评分(主 / 环 / 位 / 行 / 才 / 心)。
|
|
10
|
+
- AMC 张力分析、多点克制逻辑与十三项自检机制。
|
|
11
|
+
- 技能正文:包内 `skills/dynamic-competition-analysis-four-images-model/SKILL.md`。
|
|
12
|
+
|
|
13
|
+
## 🚀 安装
|
|
14
|
+
|
|
15
|
+
### 方式一:通过包名安装(推荐)
|
|
16
|
+
|
|
17
|
+
1. 在 DSH 插件页面点击 **「+ 添加插件」**,输入包名 `dsh-plugin-dynamic-competition-four-images`,安装并启用。
|
|
18
|
+
2. 退出并重启 DSH(宿主半边的技能桥在启动时注册)。
|
|
19
|
+
3. 安装插件**不会**把技能放进任何工作区,需要按下一节手动添加。
|
|
20
|
+
|
|
21
|
+
### 方式二:通过自定义插件市场安装
|
|
22
|
+
|
|
23
|
+
1. 开启「自定义插件市场」,添加索引源 URL:
|
|
24
|
+
`https://rootandsource.github.io/dsh-plugin-dynamic-competition-four-images/plugin-index/index.json`
|
|
25
|
+
2. 搜索「动态竞争」或「四象」,点击卡片安装并启用,然后重启 DSH。
|
|
26
|
+
|
|
27
|
+
## 🧩 把技能添加到工作区(UI)
|
|
28
|
+
|
|
29
|
+
打开 **设置 → 插件 → 四象模型** 页签:
|
|
30
|
+
|
|
31
|
+
1. 在「目标工作区」里选择一个已有工作区,或点「选择其他目录…」挑一个目录;
|
|
32
|
+
- 状态徽标会显示:未安装 / 有更新可用 / 已是最新。
|
|
33
|
+
2. 点 **「添加到工作区」**。
|
|
34
|
+
3. 成功后会提示写入的文件数,以及安装位置:
|
|
35
|
+
`<工作区>/.dsh/skills/dynamic-competition-analysis-four-images-model/`。
|
|
36
|
+
|
|
37
|
+
写入后由 DSH 原生 filesystem provider 以 source `project-dsh` 发现,即成为该工作区的工作区技能;其它工作区与全局技能列表都不受影响。
|
|
38
|
+
|
|
39
|
+
- **更新**:插件升级后再次点击「添加到工作区」,只会覆盖有变化的文件(内容一致的会跳过)。
|
|
40
|
+
- **移除**:删除该工作区里的 `.dsh/skills/dynamic-competition-analysis-four-images-model/` 目录即可。
|
|
41
|
+
|
|
42
|
+
## 🔁 从旧版(全局技能)迁移
|
|
43
|
+
|
|
44
|
+
1.1.x 及更早版本把技能复制到 `~/.dsh/skills/`,或按工作区路径自动提供技能。升级到 1.2.0 后:
|
|
45
|
+
|
|
46
|
+
- 删除旧全局副本(如果还在):
|
|
47
|
+
```powershell
|
|
48
|
+
Remove-Item -Recurse -Force "$env:USERPROFILE\.dsh\skills\dynamic-competition-analysis-four-images-model"
|
|
49
|
+
```
|
|
50
|
+
- 旧版 `workspaces` 配置项已废弃,`cordis.patch.yml` 里针对本插件的 `config.workspaces` 可以删掉。
|
|
51
|
+
- 需要在某个工作区使用技能时,用上面的 UI 重新添加。
|
|
52
|
+
|
|
53
|
+
## 🧪 校验与开发
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
node test/skill-installer.test.mjs # 受限沙箱下请直接运行单文件(node --test test/ 会因 spawn EPERM 失败)
|
|
57
|
+
npm pack --dry-run # 确认 index.js / client.js / skills / locale / icon 随包发布
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
测试覆盖:frontmatter 解析、技能发现(目录名与 name 一致)、工作区路径校验、文件复制/状态判定(缺失、过期、最新、保留工作区额外文件)、桥路由的 loopback 与只读校验、错误码。
|
|
61
|
+
|
|
62
|
+
## 📁 目录结构
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
dsh-plugin-dynamic-competition-four-images/
|
|
66
|
+
├── index.js # 宿主:技能发现 + loopback 桥(describe / install)
|
|
67
|
+
├── client.js # 客户端:设置 → 插件 → 四象模型 页签
|
|
68
|
+
├── cordis.patch.yml # bundle 行
|
|
69
|
+
├── locale/{zh,en}.json # 插件显示名与描述
|
|
70
|
+
├── icon.svg # 插件图标
|
|
71
|
+
├── package.json # dsh.bundle.patch + dsh.client
|
|
72
|
+
├── test/skill-installer.test.mjs
|
|
73
|
+
├── release/ # 打包产物(当前 + 历史版本 tarball)
|
|
74
|
+
├── plugin-index/ # 自定义插件市场索引
|
|
75
|
+
├── skills/dynamic-competition-analysis-four-images-model/
|
|
76
|
+
├── SKILL.md
|
|
77
|
+
├── reference/four-images-model-governance.md
|
|
78
|
+
├── references/full-spec.md
|
|
79
|
+
├── scripts/dynamic_competition_skill.py
|
|
80
|
+
└── examples/example_a.json, example_b.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 许可
|
|
84
|
+
|
|
85
|
+
MIT,详见 [LICENSE](LICENSE)。
|
package/client.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-dynamic-competition-four-images — Client 半边
|
|
3
|
+
*
|
|
4
|
+
* 在「设置 → 插件」里增加一个页签:列出插件内的技能,允许用户把它们
|
|
5
|
+
* 添加到指定工作区(写入 <workspace>/.dsh/skills/<name>/)。数据层走宿主
|
|
6
|
+
* 的 loopback 桥(见 index.js),不依赖任何 @Remote 命名空间。
|
|
7
|
+
*
|
|
8
|
+
* 本文件由 dsh.client 加载,注册工厂 id 必须等于包名。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: 'dsh-plugin-dynamic-competition-four-images',
|
|
13
|
+
factory(require) {
|
|
14
|
+
const React = require('react')
|
|
15
|
+
const h = React.createElement
|
|
16
|
+
|
|
17
|
+
const NS = 'dsh-plugin-dynamic-competition-four-images'
|
|
18
|
+
const BRIDGE = '/api/dsh-plugin-dynamic-competition-four-images'
|
|
19
|
+
const SLOT = 'settings.plugins.tab'
|
|
20
|
+
|
|
21
|
+
const ZH = {
|
|
22
|
+
'tab.label': '四象模型',
|
|
23
|
+
title: '四象模型分析技能',
|
|
24
|
+
lead: '把插件内携带的技能安装到指定工作区。安装后技能写入该工作区的 .dsh/skills/,只在该工作区生效,不会成为全局技能。',
|
|
25
|
+
'workspace.label': '目标工作区',
|
|
26
|
+
'workspace.placeholder': '选择工作区…',
|
|
27
|
+
'workspace.pick': '选择其他目录…',
|
|
28
|
+
'workspace.picking': '选择中…',
|
|
29
|
+
'workspace.none': '还没有选择工作区',
|
|
30
|
+
'workspace.custom': '自定义目录',
|
|
31
|
+
install: '添加到工作区',
|
|
32
|
+
installing: '正在添加…',
|
|
33
|
+
'skills.empty': '插件内没有找到可用技能。',
|
|
34
|
+
'status.unselected': '未选择工作区',
|
|
35
|
+
'status.missing': '未安装',
|
|
36
|
+
'status.outdated': '有更新可用',
|
|
37
|
+
'status.up-to-date': '已是最新',
|
|
38
|
+
'result.installed': '已添加到工作区',
|
|
39
|
+
'result.updated': '已更新',
|
|
40
|
+
'result.unchanged': '内容一致,无需更新',
|
|
41
|
+
'result.written': '写入 {written} 个文件,共 {total} 个',
|
|
42
|
+
'result.target': '安装位置:{path}',
|
|
43
|
+
'error.prefix': '操作失败:',
|
|
44
|
+
'error.describe': '无法读取插件技能列表',
|
|
45
|
+
'error.noWorkspace': '请先选择一个工作区',
|
|
46
|
+
'error.bridge': '宿主桥不可用,请重启 DSH 后重试',
|
|
47
|
+
version: '版本 {version}',
|
|
48
|
+
note: '重复添加只会覆盖有变化的文件;删除工作区里的该目录即可移除技能。',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const EN = {
|
|
52
|
+
'tab.label': 'Four Images',
|
|
53
|
+
title: 'Four Images analysis skill',
|
|
54
|
+
lead: 'Install the skill shipped with this plugin into a workspace. It is written to that workspace’s .dsh/skills/ and stays scoped to that workspace; it never becomes a global skill.',
|
|
55
|
+
'workspace.label': 'Target workspace',
|
|
56
|
+
'workspace.placeholder': 'Choose a workspace…',
|
|
57
|
+
'workspace.pick': 'Choose another folder…',
|
|
58
|
+
'workspace.picking': 'Choosing…',
|
|
59
|
+
'workspace.none': 'No workspace selected yet',
|
|
60
|
+
'workspace.custom': 'Custom folder',
|
|
61
|
+
install: 'Add to workspace',
|
|
62
|
+
installing: 'Adding…',
|
|
63
|
+
'skills.empty': 'No skill found inside this plugin.',
|
|
64
|
+
'status.unselected': 'No workspace selected',
|
|
65
|
+
'status.missing': 'Not installed',
|
|
66
|
+
'status.outdated': 'Update available',
|
|
67
|
+
'status.up-to-date': 'Up to date',
|
|
68
|
+
'result.installed': 'Added to the workspace',
|
|
69
|
+
'result.updated': 'Updated',
|
|
70
|
+
'result.unchanged': 'Already up to date',
|
|
71
|
+
'result.written': '{written} of {total} files written',
|
|
72
|
+
'result.target': 'Installed at {path}',
|
|
73
|
+
'error.prefix': 'Failed: ',
|
|
74
|
+
'error.describe': 'Could not read the skill list',
|
|
75
|
+
'error.noWorkspace': 'Choose a workspace first',
|
|
76
|
+
'error.bridge': 'Host bridge unavailable; restart DSH and retry',
|
|
77
|
+
version: 'Version {version}',
|
|
78
|
+
note: 'Re-adding only overwrites files that changed; delete that folder in the workspace to remove the skill.',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const STATUS_TONE = {
|
|
82
|
+
missing: 'var(--dsw-alias-state-warn-primary)',
|
|
83
|
+
outdated: 'var(--dsw-alias-state-warn-primary)',
|
|
84
|
+
'up-to-date': 'var(--dsw-alias-state-success-primary)',
|
|
85
|
+
unselected: 'var(--dsw-alias-state-idle-primary)',
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function format(template, params) {
|
|
89
|
+
return template.replace(/\{(\w+)\}/g, (whole, key) => (params && key in params ? String(params[key]) : whole))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 调宿主桥;返回 value,失败抛错。 */
|
|
93
|
+
async function call(op, body) {
|
|
94
|
+
const response = await fetch(`${BRIDGE}/${op}`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'content-type': 'application/json' },
|
|
97
|
+
body: JSON.stringify(body ?? {}),
|
|
98
|
+
})
|
|
99
|
+
let payload
|
|
100
|
+
try {
|
|
101
|
+
payload = await response.json()
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error(`HTTP ${response.status}`)
|
|
104
|
+
}
|
|
105
|
+
if (payload === null || typeof payload !== 'object') throw new Error(`HTTP ${response.status}`)
|
|
106
|
+
if (payload.ok !== true) throw new Error(payload.message || payload.code || `HTTP ${response.status}`)
|
|
107
|
+
return payload.value
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function workspaceList(snapshot) {
|
|
111
|
+
const raw = snapshot && (snapshot.workspaces ?? snapshot.items ?? snapshot)
|
|
112
|
+
const list = []
|
|
113
|
+
if (!Array.isArray(raw)) return list
|
|
114
|
+
for (const item of raw) {
|
|
115
|
+
if (item === null || typeof item !== 'object') continue
|
|
116
|
+
const path = typeof item.path === 'string' ? item.path : undefined
|
|
117
|
+
if (path === undefined || path === '') continue
|
|
118
|
+
const title = typeof item.title === 'string' && item.title !== '' ? item.title : path
|
|
119
|
+
list.push({ id: String(item.workspaceId ?? item.id ?? path), title, path })
|
|
120
|
+
}
|
|
121
|
+
return list
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const cardStyle = {
|
|
125
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
126
|
+
borderRadius: 12,
|
|
127
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
128
|
+
padding: '16px 18px',
|
|
129
|
+
display: 'grid',
|
|
130
|
+
gap: 14,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const labelStyle = {
|
|
134
|
+
display: 'block',
|
|
135
|
+
fontSize: 12,
|
|
136
|
+
fontWeight: 600,
|
|
137
|
+
letterSpacing: '0.02em',
|
|
138
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
139
|
+
marginBottom: 6,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const boxStyle = {
|
|
143
|
+
width: '100%',
|
|
144
|
+
boxSizing: 'border-box',
|
|
145
|
+
minHeight: 34,
|
|
146
|
+
padding: '6px 10px',
|
|
147
|
+
borderRadius: 8,
|
|
148
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
149
|
+
background: 'var(--dsw-alias-bg-base)',
|
|
150
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
151
|
+
font: 'inherit',
|
|
152
|
+
fontSize: 13,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buttonStyle(primary, disabled) {
|
|
156
|
+
return {
|
|
157
|
+
minHeight: 34,
|
|
158
|
+
padding: '6px 14px',
|
|
159
|
+
borderRadius: 8,
|
|
160
|
+
border: primary ? '1px solid transparent' : '1px solid var(--dsw-alias-border-l2)',
|
|
161
|
+
background: primary ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-bg-base)',
|
|
162
|
+
color: primary ? 'var(--dsw-alias-bg-base)' : 'var(--dsw-alias-label-primary)',
|
|
163
|
+
font: 'inherit',
|
|
164
|
+
fontSize: 13,
|
|
165
|
+
fontWeight: 600,
|
|
166
|
+
cursor: disabled ? 'default' : 'pointer',
|
|
167
|
+
opacity: disabled ? 0.55 : 1,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function badgeStyle(tone) {
|
|
172
|
+
return {
|
|
173
|
+
display: 'inline-flex',
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
gap: 6,
|
|
176
|
+
padding: '2px 8px',
|
|
177
|
+
borderRadius: 999,
|
|
178
|
+
border: `1px solid ${tone}`,
|
|
179
|
+
color: tone,
|
|
180
|
+
fontSize: 12,
|
|
181
|
+
fontWeight: 600,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
name: NS,
|
|
187
|
+
inject: ['slots'],
|
|
188
|
+
apply(ctx) {
|
|
189
|
+
const getLocale = () => {
|
|
190
|
+
try {
|
|
191
|
+
return typeof ctx.get === 'function' ? ctx.get('locale') : undefined
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const localeFace = () => {
|
|
197
|
+
const live = getLocale()
|
|
198
|
+
return live !== null && live !== undefined && typeof live.bind === 'function' ? live : null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// locale 服务是异步 install 的:apply 时可能还没就绪,所以每次用时再取,
|
|
202
|
+
// 词典只注册一次(HMR 重复注册会被运行时拒绝,这里静默忽略)。
|
|
203
|
+
let dictsRegistered = false
|
|
204
|
+
const ensureDicts = () => {
|
|
205
|
+
if (dictsRegistered) return
|
|
206
|
+
const live = localeFace()
|
|
207
|
+
if (live === null) return
|
|
208
|
+
dictsRegistered = true
|
|
209
|
+
try {
|
|
210
|
+
ctx.effect(() => live.register(NS, { zh: ZH, en: EN }))
|
|
211
|
+
} catch {
|
|
212
|
+
// 已有词典(例如插件热更新)时保持现状
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const translate = (key, params) => {
|
|
217
|
+
const live = localeFace()
|
|
218
|
+
if (live !== null) {
|
|
219
|
+
try {
|
|
220
|
+
return format(live.bind(NS)(key, params), params)
|
|
221
|
+
} catch {
|
|
222
|
+
// 绑定失败时退回内置中文
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return format(ZH[key] ?? key, params)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const uiWorkspace = () => {
|
|
229
|
+
try {
|
|
230
|
+
return typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : undefined
|
|
231
|
+
} catch {
|
|
232
|
+
return undefined
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
ensureDicts()
|
|
237
|
+
|
|
238
|
+
function Installer(props) {
|
|
239
|
+
const t = typeof props?.t === 'function' ? (key, params) => format(props.t(key, params), params) : translate
|
|
240
|
+
const snapshot =
|
|
241
|
+
props !== null && typeof props?.useWorkspaces === 'function' ? props.useWorkspaces((value) => value) : undefined
|
|
242
|
+
const workspaces = workspaceList(snapshot)
|
|
243
|
+
|
|
244
|
+
const [path, setPath] = React.useState('')
|
|
245
|
+
const [skills, setSkills] = React.useState([])
|
|
246
|
+
const [version, setVersion] = React.useState('')
|
|
247
|
+
const [error, setError] = React.useState(null)
|
|
248
|
+
const [busy, setBusy] = React.useState(false)
|
|
249
|
+
const [picking, setPicking] = React.useState(false)
|
|
250
|
+
const [notice, setNotice] = React.useState(null)
|
|
251
|
+
|
|
252
|
+
const refresh = React.useCallback(async (workspacePath) => {
|
|
253
|
+
try {
|
|
254
|
+
const value = await call('describe', workspacePath === '' ? {} : { workspacePath })
|
|
255
|
+
setSkills(Array.isArray(value.skills) ? value.skills : [])
|
|
256
|
+
setVersion(typeof value.version === 'string' ? value.version : '')
|
|
257
|
+
setError(null)
|
|
258
|
+
} catch (failure) {
|
|
259
|
+
setSkills([])
|
|
260
|
+
setError(`${t('error.describe')}:${failure instanceof Error ? failure.message : String(failure)}`)
|
|
261
|
+
}
|
|
262
|
+
}, [t])
|
|
263
|
+
|
|
264
|
+
React.useEffect(() => {
|
|
265
|
+
let cancelled = false
|
|
266
|
+
refresh(path).catch(() => {
|
|
267
|
+
if (!cancelled) setError(t('error.bridge'))
|
|
268
|
+
})
|
|
269
|
+
return () => {
|
|
270
|
+
cancelled = true
|
|
271
|
+
}
|
|
272
|
+
}, [path, refresh, t])
|
|
273
|
+
|
|
274
|
+
const pickDirectory = async () => {
|
|
275
|
+
const service = uiWorkspace()
|
|
276
|
+
if (service === undefined || typeof service.pickDirectory !== 'function') return
|
|
277
|
+
setPicking(true)
|
|
278
|
+
try {
|
|
279
|
+
const picked = await service.pickDirectory()
|
|
280
|
+
if (typeof picked === 'string' && picked !== '') {
|
|
281
|
+
setPath(picked)
|
|
282
|
+
setNotice(null)
|
|
283
|
+
}
|
|
284
|
+
} catch (failure) {
|
|
285
|
+
setNotice({ kind: 'error', text: `${t('error.prefix')}${failure instanceof Error ? failure.message : String(failure)}` })
|
|
286
|
+
} finally {
|
|
287
|
+
setPicking(false)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const install = async () => {
|
|
292
|
+
if (path === '') {
|
|
293
|
+
setNotice({ kind: 'error', text: t('error.noWorkspace') })
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
setBusy(true)
|
|
297
|
+
setNotice(null)
|
|
298
|
+
try {
|
|
299
|
+
const value = await call('install', { workspacePath: path })
|
|
300
|
+
const tone = value.status === 'unchanged' ? 'warn' : 'ok'
|
|
301
|
+
const head = t(`result.${value.status}`)
|
|
302
|
+
const detail = format(t('result.written'), { written: value.written, total: value.total })
|
|
303
|
+
setNotice({ kind: tone, text: `${head} · ${detail}`, target: format(t('result.target'), { path: value.targetPath }) })
|
|
304
|
+
await refresh(path)
|
|
305
|
+
} catch (failure) {
|
|
306
|
+
setNotice({ kind: 'error', text: `${t('error.prefix')}${failure instanceof Error ? failure.message : String(failure)}` })
|
|
307
|
+
} finally {
|
|
308
|
+
setBusy(false)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const options = workspaces.map((workspace) => ({ value: workspace.path, label: `${workspace.title} — ${workspace.path}` }))
|
|
313
|
+
if (path !== '' && !options.some((option) => option.value === path)) {
|
|
314
|
+
options.push({ value: path, label: `${t('workspace.custom')} — ${path}` })
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const noticeTone =
|
|
318
|
+
notice === null
|
|
319
|
+
? 'var(--dsw-alias-label-secondary)'
|
|
320
|
+
: notice.kind === 'error'
|
|
321
|
+
? 'var(--dsw-alias-state-error-primary)'
|
|
322
|
+
: notice.kind === 'warn'
|
|
323
|
+
? 'var(--dsw-alias-state-warn-primary)'
|
|
324
|
+
: 'var(--dsw-alias-state-success-primary)'
|
|
325
|
+
|
|
326
|
+
return h('div', { style: { display: 'grid', gap: 16, padding: '8px 0', maxWidth: 760, color: 'var(--dsw-alias-label-primary)' } }, [
|
|
327
|
+
h('div', { key: 'head' }, [
|
|
328
|
+
h('div', { key: 'title', style: { fontSize: 16, fontWeight: 600 } }, t('title')),
|
|
329
|
+
h(
|
|
330
|
+
'p',
|
|
331
|
+
{ key: 'lead', style: { margin: '6px 0 0', fontSize: 13, lineHeight: 1.6, color: 'var(--dsw-alias-label-secondary)' } },
|
|
332
|
+
t('lead'),
|
|
333
|
+
),
|
|
334
|
+
version === ''
|
|
335
|
+
? null
|
|
336
|
+
: h(
|
|
337
|
+
'p',
|
|
338
|
+
{ key: 'version', style: { margin: '4px 0 0', fontSize: 12, color: 'var(--dsw-alias-state-idle-primary)' } },
|
|
339
|
+
format(t('version'), { version }),
|
|
340
|
+
),
|
|
341
|
+
]),
|
|
342
|
+
|
|
343
|
+
h('div', { key: 'card', style: cardStyle }, [
|
|
344
|
+
h('div', { key: 'skills', style: { display: 'grid', gap: 8 } }, [
|
|
345
|
+
skills.length === 0
|
|
346
|
+
? h('div', { key: 'empty', style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, error ?? t('skills.empty'))
|
|
347
|
+
: null,
|
|
348
|
+
...skills.map((skill) =>
|
|
349
|
+
h('div', { key: skill.name, style: { display: 'grid', gap: 4 } }, [
|
|
350
|
+
h('div', { key: 'line', style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' } }, [
|
|
351
|
+
h('code', { key: 'name', style: { fontSize: 13, fontWeight: 600 } }, skill.name),
|
|
352
|
+
h('span', { key: 'badge', style: badgeStyle(STATUS_TONE[skill.status] ?? STATUS_TONE.unselected) }, t(`status.${skill.status}`)),
|
|
353
|
+
]),
|
|
354
|
+
h(
|
|
355
|
+
'div',
|
|
356
|
+
{ key: 'desc', style: { fontSize: 12, lineHeight: 1.6, color: 'var(--dsw-alias-label-secondary)' } },
|
|
357
|
+
skill.description,
|
|
358
|
+
),
|
|
359
|
+
]),
|
|
360
|
+
),
|
|
361
|
+
]),
|
|
362
|
+
|
|
363
|
+
h('div', { key: 'picker', style: { display: 'grid', gap: 8 } }, [
|
|
364
|
+
h('label', { key: 'label', style: labelStyle, htmlFor: `${NS}-workspace` }, t('workspace.label')),
|
|
365
|
+
h('div', { key: 'row', style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, [
|
|
366
|
+
h(
|
|
367
|
+
'select',
|
|
368
|
+
{
|
|
369
|
+
key: 'select',
|
|
370
|
+
id: `${NS}-workspace`,
|
|
371
|
+
value: path,
|
|
372
|
+
style: { ...boxStyle, flex: '1 1 260px' },
|
|
373
|
+
onChange: (event) => {
|
|
374
|
+
setPath(event.target.value)
|
|
375
|
+
setNotice(null)
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
[
|
|
379
|
+
h('option', { key: 'placeholder', value: '' }, t('workspace.placeholder')),
|
|
380
|
+
...options.map((option) => h('option', { key: option.value, value: option.value }, option.label)),
|
|
381
|
+
],
|
|
382
|
+
),
|
|
383
|
+
h(
|
|
384
|
+
'button',
|
|
385
|
+
{ key: 'pick', type: 'button', style: buttonStyle(false, picking), disabled: picking, onClick: pickDirectory },
|
|
386
|
+
picking ? t('workspace.picking') : t('workspace.pick'),
|
|
387
|
+
),
|
|
388
|
+
]),
|
|
389
|
+
path === ''
|
|
390
|
+
? h('div', { key: 'none', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('workspace.none'))
|
|
391
|
+
: h('div', { key: 'path', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', wordBreak: 'break-all' } }, path),
|
|
392
|
+
]),
|
|
393
|
+
|
|
394
|
+
h('div', { key: 'actions', style: { display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } }, [
|
|
395
|
+
h(
|
|
396
|
+
'button',
|
|
397
|
+
{
|
|
398
|
+
key: 'install',
|
|
399
|
+
type: 'button',
|
|
400
|
+
style: buttonStyle(true, busy || path === '' || skills.length === 0),
|
|
401
|
+
disabled: busy || path === '' || skills.length === 0,
|
|
402
|
+
onClick: install,
|
|
403
|
+
},
|
|
404
|
+
busy ? t('installing') : t('install'),
|
|
405
|
+
),
|
|
406
|
+
notice === null
|
|
407
|
+
? null
|
|
408
|
+
: h(
|
|
409
|
+
'div',
|
|
410
|
+
{ key: 'notice', style: { fontSize: 12, lineHeight: 1.6, color: noticeTone, display: 'grid', gap: 2 } }, [
|
|
411
|
+
h('div', { key: 'text' }, notice.text),
|
|
412
|
+
notice.target === undefined ? null : h('div', { key: 'target', style: { color: 'var(--dsw-alias-label-secondary)', wordBreak: 'break-all' } }, notice.target),
|
|
413
|
+
],
|
|
414
|
+
),
|
|
415
|
+
]),
|
|
416
|
+
|
|
417
|
+
h(
|
|
418
|
+
'p',
|
|
419
|
+
{ key: 'note', style: { margin: 0, fontSize: 12, lineHeight: 1.6, color: 'var(--dsw-alias-label-secondary)' } },
|
|
420
|
+
t('note'),
|
|
421
|
+
),
|
|
422
|
+
]),
|
|
423
|
+
])
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
ctx.slots.inject(SLOT, () => {
|
|
427
|
+
// 设置面板打开时 locale 一定已就绪:补充注册词典,并按现状决定 locale 席位
|
|
428
|
+
ensureDicts()
|
|
429
|
+
const registration = {
|
|
430
|
+
name: SLOT,
|
|
431
|
+
id: 'dynamic-competition-four-images',
|
|
432
|
+
order: 40,
|
|
433
|
+
label: () => translate('tab.label'),
|
|
434
|
+
...(localeFace() === null ? {} : { locale: NS }),
|
|
435
|
+
}
|
|
436
|
+
return ctx.slots.register(registration, Installer)
|
|
437
|
+
})
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
})
|
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# dsh-plugin-dynamic-competition-four-images bundle patch.
|
|
2
|
+
#
|
|
3
|
+
# 插件本身全局安装;技能不会自动出现在任何工作区。
|
|
4
|
+
# 用户在「设置 → 插件 → 四象模型」页签里选择目标工作区,点击“添加到工作区”,
|
|
5
|
+
# 宿主才把技能写入 <workspace>/.dsh/skills/<name>/。
|
|
6
|
+
- insert:
|
|
7
|
+
- id: dynamic-competition-four-images
|
|
8
|
+
name: dsh-plugin-dynamic-competition-four-images
|
package/icon.svg
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Four Images">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#0f172a"/>
|
|
3
|
+
<g fill="none" stroke="#e6edf7" stroke-width="2" opacity="0.85">
|
|
4
|
+
<rect x="14" y="14" width="16" height="16" rx="4"/>
|
|
5
|
+
<rect x="34" y="14" width="16" height="16" rx="4"/>
|
|
6
|
+
<rect x="14" y="34" width="16" height="16" rx="4"/>
|
|
7
|
+
<rect x="34" y="34" width="16" height="16" rx="4"/>
|
|
8
|
+
</g>
|
|
9
|
+
<path d="M22 22h.01" stroke="#7dd3fc" stroke-width="8" stroke-linecap="round"/>
|
|
10
|
+
<path d="M42 22h.01" stroke="#fbbf24" stroke-width="8" stroke-linecap="round"/>
|
|
11
|
+
<path d="M22 42h.01" stroke="#f87171" stroke-width="8" stroke-linecap="round"/>
|
|
12
|
+
<path d="M42 42h.01" stroke="#34d399" stroke-width="8" stroke-linecap="round"/>
|
|
13
|
+
</svg>
|