dsh-muv-table 0.2.1 → 0.2.3
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 +68 -20
- package/LICENSE +85 -85
- package/LICENSE.md +9 -0
- package/README.md +59 -59
- package/cordis.patch.yml +5 -5
- package/lib/block-generator.js +56 -56
- package/lib/client.js +419 -419
- package/lib/index.js +44 -4
- package/lib/initvar-parser.js +100 -100
- package/lib/muv-parser.js +121 -121
- package/package.json +21 -20
package/lib/index.js
CHANGED
|
@@ -12,8 +12,46 @@ import { fileURLToPath } from 'node:url'
|
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
13
13
|
let PANEL_HTML = ''
|
|
14
14
|
try { PANEL_HTML = fs.readFileSync(path.join(__dirname, 'panel.html'), 'utf8') } catch (_) {}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
// ── DSH home 解析 ─────────────────────────────────────────
|
|
17
|
+
// 与 @deepseek-ai/dsh-home-paths 的 resolveDshHome() 一致:
|
|
18
|
+
// 优先级 = 显式配置 → $DSH_HOME(非空)→ ~/.dsh。
|
|
19
|
+
// 用户预设目录 = <dshHome>/.agent-presets(酒馆插件也用同一份)。
|
|
20
|
+
// 写死 ~/.dsh 会让 DSH_HOME 指向非默认位置的部署读不到预设。
|
|
21
|
+
function expandHomePrefix(p) {
|
|
22
|
+
if (p === '~') return os.homedir()
|
|
23
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(os.homedir(), p.slice(2))
|
|
24
|
+
return p
|
|
25
|
+
}
|
|
26
|
+
function resolveDshHome() {
|
|
27
|
+
const env = process.env.DSH_HOME
|
|
28
|
+
if (typeof env === 'string' && env.trim().length > 0) return path.resolve(expandHomePrefix(env.trim()))
|
|
29
|
+
return path.join(os.homedir(), '.dsh')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// apply() 时按 DSH 实际 home 重新绑定
|
|
33
|
+
let DSH_HOME = resolveDshHome()
|
|
34
|
+
let PRESETS_ROOT = path.join(DSH_HOME, '.agent-presets')
|
|
35
|
+
let TAVERN_PRESET_DIR = path.join(PRESETS_ROOT, 'tavern-lite')
|
|
36
|
+
let SESSION_BINDINGS_FILE = path.join(PRESETS_ROOT, 'session-bindings.json')
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 绑定路径到 DSH 实际 home:优先用 DSH 的 dshHomePath 服务,拿不到再自行解析。
|
|
40
|
+
* @param {object} ctx - cordis 上下文
|
|
41
|
+
*/
|
|
42
|
+
function bindDshPaths(ctx) {
|
|
43
|
+
let home = ''
|
|
44
|
+
try {
|
|
45
|
+
const fn = ctx && typeof ctx.get === 'function' ? ctx.get('dshHomePath') : undefined
|
|
46
|
+
if (typeof fn === 'function') home = String(fn() || '')
|
|
47
|
+
} catch {}
|
|
48
|
+
if (!home) home = resolveDshHome()
|
|
49
|
+
DSH_HOME = home
|
|
50
|
+
PRESETS_ROOT = path.join(home, '.agent-presets')
|
|
51
|
+
TAVERN_PRESET_DIR = path.join(PRESETS_ROOT, 'tavern-lite')
|
|
52
|
+
SESSION_BINDINGS_FILE = path.join(PRESETS_ROOT, 'session-bindings.json')
|
|
53
|
+
}
|
|
54
|
+
|
|
17
55
|
const MUV_CARD_DIRS = ['E:/BaiduNetdiskDownload/EdgeDownload']
|
|
18
56
|
|
|
19
57
|
/** Read a YAML/yml file with encoding detection (GBK → UTF-8 fallback). */
|
|
@@ -64,7 +102,7 @@ function scanAllPresetCards() {
|
|
|
64
102
|
.filter(d => d.isDirectory())
|
|
65
103
|
.map(d => path.join(PRESETS_ROOT, d.name))
|
|
66
104
|
// Also scan the DSH root (presets like anchored-standard live there too)
|
|
67
|
-
for (const dir of [...dirs,
|
|
105
|
+
for (const dir of [...dirs, DSH_HOME]) {
|
|
68
106
|
const ymlPath = path.join(dir, 'agent.cordis.yml')
|
|
69
107
|
if (!fs.existsSync(ymlPath)) continue
|
|
70
108
|
try {
|
|
@@ -81,7 +119,7 @@ function scanAllPresetCards() {
|
|
|
81
119
|
/** Resolve preset dir for a session id via session-bindings.json */
|
|
82
120
|
function presetDirForSession(sessionId) {
|
|
83
121
|
try {
|
|
84
|
-
const bindPath =
|
|
122
|
+
const bindPath = SESSION_BINDINGS_FILE
|
|
85
123
|
if (sessionId && fs.existsSync(bindPath)) {
|
|
86
124
|
const bind = JSON.parse(fs.readFileSync(bindPath, 'utf8'))
|
|
87
125
|
const preset = bind[sessionId]
|
|
@@ -188,6 +226,8 @@ function readBody(req) {
|
|
|
188
226
|
}
|
|
189
227
|
|
|
190
228
|
export function apply(ctx) {
|
|
229
|
+
bindDshPaths(ctx)
|
|
230
|
+
console.log('[dsh-muv-table] DSH_HOME = ' + DSH_HOME + ' 预设目录 = ' + PRESETS_ROOT)
|
|
191
231
|
const routes = [
|
|
192
232
|
{
|
|
193
233
|
kind: 'exact',
|
package/lib/initvar-parser.js
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
// MUV Initvar parser: converts YAML-like indented initvar text to nested objects.
|
|
2
|
-
// Format: 2-space indentation, `键: 值` pairs. Supports nested objects and inline
|
|
3
|
-
// lists (comma-separated values wrapped in [ ]).
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Parse an initvar text block into a nested object.
|
|
7
|
-
* @param {string} text - The initvar text (between <initvar> and </initvar>)
|
|
8
|
-
* @returns {object} Parsed nested object
|
|
9
|
-
*/
|
|
10
|
-
export function parseInitvar(text) {
|
|
11
|
-
const lines = text.split(/\r?\n/)
|
|
12
|
-
const root = {}
|
|
13
|
-
const stack = [{ obj: root, indent: -1 }]
|
|
14
|
-
|
|
15
|
-
for (const rawLine of lines) {
|
|
16
|
-
const line = rawLine.trimEnd()
|
|
17
|
-
if (line.trim() === '') continue
|
|
18
|
-
|
|
19
|
-
const indent = line.length - line.trimStart().length
|
|
20
|
-
const trimmed = line.trim()
|
|
21
|
-
|
|
22
|
-
// Pop stack until we find the right parent
|
|
23
|
-
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
24
|
-
stack.pop()
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const parent = stack[stack.length - 1].obj
|
|
28
|
-
const colonIdx = trimmed.indexOf(':')
|
|
29
|
-
|
|
30
|
-
if (colonIdx === -1) {
|
|
31
|
-
// Value-only line (continuation or list item)
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const key = trimmed.slice(0, colonIdx).trim()
|
|
36
|
-
const value = trimmed.slice(colonIdx + 1).trim()
|
|
37
|
-
|
|
38
|
-
if (value === '') {
|
|
39
|
-
// Nested object start
|
|
40
|
-
const newObj = {}
|
|
41
|
-
parent[key] = newObj
|
|
42
|
-
stack.push({ obj: newObj, indent })
|
|
43
|
-
} else {
|
|
44
|
-
// Leaf value
|
|
45
|
-
parent[key] = parseValue(value)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return root
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Parse a leaf value string to appropriate type.
|
|
54
|
-
* @param {string} val
|
|
55
|
-
* @returns {string|number|boolean}
|
|
56
|
-
*/
|
|
57
|
-
function parseValue(val) {
|
|
58
|
-
// Empty string
|
|
59
|
-
if (val === '""' || val === "''") return ''
|
|
60
|
-
if (val === '{}') return '{}'
|
|
61
|
-
|
|
62
|
-
// Number
|
|
63
|
-
if (/^-?\d+(\.\d+)?$/.test(val)) {
|
|
64
|
-
return Number(val)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Boolean
|
|
68
|
-
if (val === 'true') return true
|
|
69
|
-
if (val === 'false') return false
|
|
70
|
-
|
|
71
|
-
return val
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Serialize a nested object back to initvar text.
|
|
76
|
-
* @param {object} obj
|
|
77
|
-
* @param {number} [depth=0]
|
|
78
|
-
* @returns {string}
|
|
79
|
-
*/
|
|
80
|
-
export function serializeInitvar(obj, depth = 0) {
|
|
81
|
-
const indent = ' '.repeat(depth)
|
|
82
|
-
let result = ''
|
|
83
|
-
|
|
84
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
85
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
86
|
-
result += `${indent}${key}:\n`
|
|
87
|
-
result += serializeInitvar(value, depth + 1)
|
|
88
|
-
} else {
|
|
89
|
-
result += `${indent}${key}: ${formatValue(value)}\n`
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return result
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function formatValue(val) {
|
|
97
|
-
if (val === null || val === undefined) return ''
|
|
98
|
-
if (typeof val === 'boolean') return val ? 'true' : 'false'
|
|
99
|
-
if (typeof val === 'number') return String(val)
|
|
100
|
-
return String(val)
|
|
1
|
+
// MUV Initvar parser: converts YAML-like indented initvar text to nested objects.
|
|
2
|
+
// Format: 2-space indentation, `键: 值` pairs. Supports nested objects and inline
|
|
3
|
+
// lists (comma-separated values wrapped in [ ]).
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse an initvar text block into a nested object.
|
|
7
|
+
* @param {string} text - The initvar text (between <initvar> and </initvar>)
|
|
8
|
+
* @returns {object} Parsed nested object
|
|
9
|
+
*/
|
|
10
|
+
export function parseInitvar(text) {
|
|
11
|
+
const lines = text.split(/\r?\n/)
|
|
12
|
+
const root = {}
|
|
13
|
+
const stack = [{ obj: root, indent: -1 }]
|
|
14
|
+
|
|
15
|
+
for (const rawLine of lines) {
|
|
16
|
+
const line = rawLine.trimEnd()
|
|
17
|
+
if (line.trim() === '') continue
|
|
18
|
+
|
|
19
|
+
const indent = line.length - line.trimStart().length
|
|
20
|
+
const trimmed = line.trim()
|
|
21
|
+
|
|
22
|
+
// Pop stack until we find the right parent
|
|
23
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
24
|
+
stack.pop()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const parent = stack[stack.length - 1].obj
|
|
28
|
+
const colonIdx = trimmed.indexOf(':')
|
|
29
|
+
|
|
30
|
+
if (colonIdx === -1) {
|
|
31
|
+
// Value-only line (continuation or list item)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const key = trimmed.slice(0, colonIdx).trim()
|
|
36
|
+
const value = trimmed.slice(colonIdx + 1).trim()
|
|
37
|
+
|
|
38
|
+
if (value === '') {
|
|
39
|
+
// Nested object start
|
|
40
|
+
const newObj = {}
|
|
41
|
+
parent[key] = newObj
|
|
42
|
+
stack.push({ obj: newObj, indent })
|
|
43
|
+
} else {
|
|
44
|
+
// Leaf value
|
|
45
|
+
parent[key] = parseValue(value)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return root
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse a leaf value string to appropriate type.
|
|
54
|
+
* @param {string} val
|
|
55
|
+
* @returns {string|number|boolean}
|
|
56
|
+
*/
|
|
57
|
+
function parseValue(val) {
|
|
58
|
+
// Empty string
|
|
59
|
+
if (val === '""' || val === "''") return ''
|
|
60
|
+
if (val === '{}') return '{}'
|
|
61
|
+
|
|
62
|
+
// Number
|
|
63
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) {
|
|
64
|
+
return Number(val)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Boolean
|
|
68
|
+
if (val === 'true') return true
|
|
69
|
+
if (val === 'false') return false
|
|
70
|
+
|
|
71
|
+
return val
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Serialize a nested object back to initvar text.
|
|
76
|
+
* @param {object} obj
|
|
77
|
+
* @param {number} [depth=0]
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function serializeInitvar(obj, depth = 0) {
|
|
81
|
+
const indent = ' '.repeat(depth)
|
|
82
|
+
let result = ''
|
|
83
|
+
|
|
84
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
85
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
86
|
+
result += `${indent}${key}:\n`
|
|
87
|
+
result += serializeInitvar(value, depth + 1)
|
|
88
|
+
} else {
|
|
89
|
+
result += `${indent}${key}: ${formatValue(value)}\n`
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatValue(val) {
|
|
97
|
+
if (val === null || val === undefined) return ''
|
|
98
|
+
if (typeof val === 'boolean') return val ? 'true' : 'false'
|
|
99
|
+
if (typeof val === 'number') return String(val)
|
|
100
|
+
return String(val)
|
|
101
101
|
}
|
package/lib/muv-parser.js
CHANGED
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
// MUV Schema Parser: extracts variable structure from character card JSON.
|
|
2
|
-
// Uses initvar data to infer the schema tree (more reliable than regex-parsing Zod).
|
|
3
|
-
|
|
4
|
-
import { parseInitvar } from './initvar-parser.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Parse a MUV character card JSON and extract schema + variable data.
|
|
8
|
-
* @param {object} cardJson - The full character card JSON
|
|
9
|
-
* @returns {{ name: string, schemas: object[], initvarData: object, zodSource: string }}
|
|
10
|
-
*/
|
|
11
|
-
export function parseMuvCard(cardJson) {
|
|
12
|
-
const name = cardJson?.data?.name || cardJson?.name || 'Unnamed'
|
|
13
|
-
const extensions = cardJson?.data?.extensions
|
|
14
|
-
const tavernHelper = extensions?.tavern_helper
|
|
15
|
-
|
|
16
|
-
// Extract Zod schema source (for reference)
|
|
17
|
-
const zodScript = tavernHelper?.scripts?.find(s => s.name === 'zod')
|
|
18
|
-
const zodSource = zodScript?.content || ''
|
|
19
|
-
|
|
20
|
-
// Extract initvar blocks from alternate_greetings
|
|
21
|
-
const greetings = cardJson?.data?.alternate_greetings || []
|
|
22
|
-
const initvarBlocks = []
|
|
23
|
-
|
|
24
|
-
for (const greeting of greetings) {
|
|
25
|
-
if (typeof greeting !== 'string') continue
|
|
26
|
-
const match = greeting.match(/<initvar>([\s\S]*?)<\/initvar>/i)
|
|
27
|
-
if (match) {
|
|
28
|
-
initvarBlocks.push({
|
|
29
|
-
raw: match[1].trim(),
|
|
30
|
-
parsed: parseInitvar(match[1])
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Use the first initvar block as default data
|
|
36
|
-
const initvarData = initvarBlocks.length > 0 ? initvarBlocks[0].parsed : {}
|
|
37
|
-
|
|
38
|
-
// Infer schema from initvar data
|
|
39
|
-
const schemas = inferSchemaFromData(initvarData)
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
name,
|
|
43
|
-
schemas,
|
|
44
|
-
initvarData,
|
|
45
|
-
initvarBlocks: initvarBlocks.map(b => b.raw),
|
|
46
|
-
zodSource
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Infer a schema tree from a nested data object.
|
|
52
|
-
* @param {object} data
|
|
53
|
-
* @param {string} [name='']
|
|
54
|
-
* @returns {object[]}
|
|
55
|
-
*/
|
|
56
|
-
function inferSchemaFromData(data, name = '') {
|
|
57
|
-
if (!data || typeof data !== 'object') return []
|
|
58
|
-
|
|
59
|
-
const fields = []
|
|
60
|
-
for (const [key, value] of Object.entries(data)) {
|
|
61
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
62
|
-
// Nested object
|
|
63
|
-
const children = inferSchemaFromData(value, key)
|
|
64
|
-
if (children.length > 0) {
|
|
65
|
-
fields.push({
|
|
66
|
-
name: key,
|
|
67
|
-
type: 'object',
|
|
68
|
-
defaultValue: '{}',
|
|
69
|
-
description: '',
|
|
70
|
-
children
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
} else if (typeof value === 'number') {
|
|
74
|
-
fields.push({
|
|
75
|
-
name: key,
|
|
76
|
-
type: 'number',
|
|
77
|
-
defaultValue: String(value),
|
|
78
|
-
description: '',
|
|
79
|
-
children: []
|
|
80
|
-
})
|
|
81
|
-
} else {
|
|
82
|
-
fields.push({
|
|
83
|
-
name: key,
|
|
84
|
-
type: 'string',
|
|
85
|
-
defaultValue: String(value ?? ''),
|
|
86
|
-
description: '',
|
|
87
|
-
children: []
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return fields
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Walk the schema tree and extract values from data into flattened rows.
|
|
96
|
-
* @param {object[]} schemas
|
|
97
|
-
* @param {object} data
|
|
98
|
-
* @param {string} [prefix='']
|
|
99
|
-
* @returns {object[]}
|
|
100
|
-
*/
|
|
101
|
-
export function flattenSchemaData(schemas, data, prefix = '') {
|
|
102
|
-
const rows = []
|
|
103
|
-
for (const field of schemas) {
|
|
104
|
-
const path = prefix ? `${prefix}.${field.name}` : field.name
|
|
105
|
-
const value = data?.[field.name]
|
|
106
|
-
|
|
107
|
-
if (field.type === 'object' && field.children && field.children.length > 0) {
|
|
108
|
-
rows.push({ path, key: field.name, type: 'object', value: '(object)', isContainer: true, depth: prefix.split('.').filter(Boolean).length })
|
|
109
|
-
rows.push(...flattenSchemaData(field.children, value || {}, path))
|
|
110
|
-
} else {
|
|
111
|
-
rows.push({
|
|
112
|
-
path,
|
|
113
|
-
key: field.name,
|
|
114
|
-
type: field.type,
|
|
115
|
-
value: value ?? field.defaultValue ?? '',
|
|
116
|
-
isContainer: false,
|
|
117
|
-
depth: prefix.split('.').filter(Boolean).length
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return rows
|
|
1
|
+
// MUV Schema Parser: extracts variable structure from character card JSON.
|
|
2
|
+
// Uses initvar data to infer the schema tree (more reliable than regex-parsing Zod).
|
|
3
|
+
|
|
4
|
+
import { parseInitvar } from './initvar-parser.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse a MUV character card JSON and extract schema + variable data.
|
|
8
|
+
* @param {object} cardJson - The full character card JSON
|
|
9
|
+
* @returns {{ name: string, schemas: object[], initvarData: object, zodSource: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function parseMuvCard(cardJson) {
|
|
12
|
+
const name = cardJson?.data?.name || cardJson?.name || 'Unnamed'
|
|
13
|
+
const extensions = cardJson?.data?.extensions
|
|
14
|
+
const tavernHelper = extensions?.tavern_helper
|
|
15
|
+
|
|
16
|
+
// Extract Zod schema source (for reference)
|
|
17
|
+
const zodScript = tavernHelper?.scripts?.find(s => s.name === 'zod')
|
|
18
|
+
const zodSource = zodScript?.content || ''
|
|
19
|
+
|
|
20
|
+
// Extract initvar blocks from alternate_greetings
|
|
21
|
+
const greetings = cardJson?.data?.alternate_greetings || []
|
|
22
|
+
const initvarBlocks = []
|
|
23
|
+
|
|
24
|
+
for (const greeting of greetings) {
|
|
25
|
+
if (typeof greeting !== 'string') continue
|
|
26
|
+
const match = greeting.match(/<initvar>([\s\S]*?)<\/initvar>/i)
|
|
27
|
+
if (match) {
|
|
28
|
+
initvarBlocks.push({
|
|
29
|
+
raw: match[1].trim(),
|
|
30
|
+
parsed: parseInitvar(match[1])
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use the first initvar block as default data
|
|
36
|
+
const initvarData = initvarBlocks.length > 0 ? initvarBlocks[0].parsed : {}
|
|
37
|
+
|
|
38
|
+
// Infer schema from initvar data
|
|
39
|
+
const schemas = inferSchemaFromData(initvarData)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
schemas,
|
|
44
|
+
initvarData,
|
|
45
|
+
initvarBlocks: initvarBlocks.map(b => b.raw),
|
|
46
|
+
zodSource
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Infer a schema tree from a nested data object.
|
|
52
|
+
* @param {object} data
|
|
53
|
+
* @param {string} [name='']
|
|
54
|
+
* @returns {object[]}
|
|
55
|
+
*/
|
|
56
|
+
function inferSchemaFromData(data, name = '') {
|
|
57
|
+
if (!data || typeof data !== 'object') return []
|
|
58
|
+
|
|
59
|
+
const fields = []
|
|
60
|
+
for (const [key, value] of Object.entries(data)) {
|
|
61
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
62
|
+
// Nested object
|
|
63
|
+
const children = inferSchemaFromData(value, key)
|
|
64
|
+
if (children.length > 0) {
|
|
65
|
+
fields.push({
|
|
66
|
+
name: key,
|
|
67
|
+
type: 'object',
|
|
68
|
+
defaultValue: '{}',
|
|
69
|
+
description: '',
|
|
70
|
+
children
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
} else if (typeof value === 'number') {
|
|
74
|
+
fields.push({
|
|
75
|
+
name: key,
|
|
76
|
+
type: 'number',
|
|
77
|
+
defaultValue: String(value),
|
|
78
|
+
description: '',
|
|
79
|
+
children: []
|
|
80
|
+
})
|
|
81
|
+
} else {
|
|
82
|
+
fields.push({
|
|
83
|
+
name: key,
|
|
84
|
+
type: 'string',
|
|
85
|
+
defaultValue: String(value ?? ''),
|
|
86
|
+
description: '',
|
|
87
|
+
children: []
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return fields
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Walk the schema tree and extract values from data into flattened rows.
|
|
96
|
+
* @param {object[]} schemas
|
|
97
|
+
* @param {object} data
|
|
98
|
+
* @param {string} [prefix='']
|
|
99
|
+
* @returns {object[]}
|
|
100
|
+
*/
|
|
101
|
+
export function flattenSchemaData(schemas, data, prefix = '') {
|
|
102
|
+
const rows = []
|
|
103
|
+
for (const field of schemas) {
|
|
104
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name
|
|
105
|
+
const value = data?.[field.name]
|
|
106
|
+
|
|
107
|
+
if (field.type === 'object' && field.children && field.children.length > 0) {
|
|
108
|
+
rows.push({ path, key: field.name, type: 'object', value: '(object)', isContainer: true, depth: prefix.split('.').filter(Boolean).length })
|
|
109
|
+
rows.push(...flattenSchemaData(field.children, value || {}, path))
|
|
110
|
+
} else {
|
|
111
|
+
rows.push({
|
|
112
|
+
path,
|
|
113
|
+
key: field.name,
|
|
114
|
+
type: field.type,
|
|
115
|
+
value: value ?? field.defaultValue ?? '',
|
|
116
|
+
isContainer: false,
|
|
117
|
+
depth: prefix.split('.').filter(Boolean).length
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return rows
|
|
122
122
|
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-muv-table",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "MUV 变量表格编辑器 —— 树形变量面板、宏测试浮窗(random/pick/roll)、骰子快捷栏、pick 缓存管理",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./lib/index.js",
|
|
9
|
-
"./client": "./lib/client.js",
|
|
10
|
-
"./
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-muv-table",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "MUV 变量表格编辑器 —— 树形变量面板、宏测试浮窗(random/pick/roll)、骰子快捷栏、pick 缓存管理",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./lib/*": "./lib/*",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": ["lib", "cordis.patch.yml", "CHANGELOG.md"],
|
|
14
|
+
"keywords": ["dsh", "dsh-plugin", "deepseek-harness", "muv", "magvarupdate", "tavern", "variable-table"],
|
|
15
|
+
"license": "PolyForm-Noncommercial-Copyleft-1.0.0",
|
|
16
|
+
"dsh": {
|
|
17
|
+
"bundle": { "patch": "./cordis.patch.yml" },
|
|
18
|
+
"client": { "platform": "web" }
|
|
19
|
+
},
|
|
20
|
+
"repository": {"type": "git", "url": "git+https://github.com/chen731215-dev/dsh-muv-table.git"},
|
|
21
|
+
"engines": { "node": ">=20" }
|
|
21
22
|
}
|