dsh-miloco-bridge 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -10
- package/cordis.patch.yml +1 -3
- package/{doc → docs}/ARCHITECTURE.md +2 -2
- package/{doc → docs}/INSTALL.md +8 -5
- package/{doc → docs}/SKILL-REWRITES.md +3 -3
- package/{doc → docs}/TOOLS.md +13 -10
- package/{doc → docs}/VERIFICATION.md +22 -6
- package/docs/images/agent-presets.png +0 -0
- package/docs/images/miloco-settings.png +0 -0
- package/lib/client.js +672 -119
- package/lib/settings-api.js +86 -1
- package/lib/tools.js +307 -40
- package/package.json +2 -2
- package/skills/miloco-create-task/SKILL.md +63 -14
- package/doc/images/agent-presets.png +0 -0
- package/doc/images/miloco-settings.png +0 -0
package/lib/settings-api.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { access } from 'node:fs/promises'
|
|
24
|
-
import { join } from 'node:path'
|
|
24
|
+
import { dirname, join } from 'node:path'
|
|
25
25
|
import { MilocoError, readMilocoConfig } from './miloco-client.js'
|
|
26
26
|
import { normalizePerceptionLevel, PERCEPTION_LEVELS } from './perception.js'
|
|
27
27
|
import { AGENT_MODES, describeMode, readSessions } from './state.js'
|
|
@@ -470,6 +470,91 @@ export function registerSettingsApi(options) {
|
|
|
470
470
|
})
|
|
471
471
|
})
|
|
472
472
|
|
|
473
|
+
route(`${SETTINGS_PREFIX}/version`, async (req, res) => {
|
|
474
|
+
if (req.method !== 'GET') {
|
|
475
|
+
sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
const force = new URL(req.url, 'http://127.0.0.1').searchParams.get('refresh') === '1'
|
|
479
|
+
const endpoint = await client.endpoint()
|
|
480
|
+
const fileConfig = await readMilocoConfig(endpoint.home)
|
|
481
|
+
const pythonBin = String(fileConfig?.server?.python_bin ?? '')
|
|
482
|
+
// server.python_bin = <source>/backend/.venv/bin/python
|
|
483
|
+
const sourceRoot = pythonBin
|
|
484
|
+
? dirname(dirname(dirname(dirname(pythonBin))))
|
|
485
|
+
: (config.deploy?.sourceDir || '')
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
const [version, upgrade, upgradeStatus] = await Promise.all([
|
|
489
|
+
client.get('/api/admin/version', { signal: AbortSignal.timeout(8_000) }),
|
|
490
|
+
client.get('/api/admin/upgrade/check', {
|
|
491
|
+
query: { force: force ? 'true' : 'false' },
|
|
492
|
+
signal: AbortSignal.timeout(15_000),
|
|
493
|
+
}).catch(() => null),
|
|
494
|
+
client.get('/api/admin/upgrade/status', { signal: AbortSignal.timeout(8_000) }).catch(() => null),
|
|
495
|
+
])
|
|
496
|
+
sendJson(res, 200, {
|
|
497
|
+
ok: true,
|
|
498
|
+
reachable: true,
|
|
499
|
+
sourceRoot,
|
|
500
|
+
version,
|
|
501
|
+
upgrade,
|
|
502
|
+
upgradeStatus,
|
|
503
|
+
})
|
|
504
|
+
} catch (error) {
|
|
505
|
+
sendJson(res, 200, {
|
|
506
|
+
ok: true,
|
|
507
|
+
reachable: false,
|
|
508
|
+
sourceRoot,
|
|
509
|
+
error: error instanceof MilocoError ? error.message : String(error),
|
|
510
|
+
version: null,
|
|
511
|
+
upgrade: null,
|
|
512
|
+
upgradeStatus: null,
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
route(`${SETTINGS_PREFIX}/upgrade`, async (req, res) => {
|
|
518
|
+
if (req.method !== 'POST') {
|
|
519
|
+
sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
const body = await mutateBody(req, res)
|
|
523
|
+
if (body === null) return
|
|
524
|
+
// Upstream only allows /upgrade/run for release deployments. It performs
|
|
525
|
+
// its own has_update check; surface its 400 reason verbatim.
|
|
526
|
+
try {
|
|
527
|
+
const data = await client.post('/api/admin/upgrade/run', undefined, {
|
|
528
|
+
signal: AbortSignal.timeout(30_000),
|
|
529
|
+
})
|
|
530
|
+
sendJson(res, 200, { ok: true, data })
|
|
531
|
+
} catch (error) {
|
|
532
|
+
sendJson(res, 400, {
|
|
533
|
+
ok: false,
|
|
534
|
+
error: error instanceof MilocoError ? error.message : String(error),
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
route(`${SETTINGS_PREFIX}/upgrade/status`, async (req, res) => {
|
|
540
|
+
if (req.method !== 'GET') {
|
|
541
|
+
sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
const status = await client.get('/api/admin/upgrade/status', {
|
|
546
|
+
signal: AbortSignal.timeout(8_000),
|
|
547
|
+
})
|
|
548
|
+
sendJson(res, 200, { ok: true, status })
|
|
549
|
+
} catch (error) {
|
|
550
|
+
sendJson(res, 200, {
|
|
551
|
+
ok: true,
|
|
552
|
+
status: null,
|
|
553
|
+
error: error instanceof MilocoError ? error.message : String(error),
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
|
|
473
558
|
route(`${SETTINGS_PREFIX}/deploy`, async (req, res) => {
|
|
474
559
|
if (!options.deploy) {
|
|
475
560
|
sendJson(res, 503, { ok: false, error: '部署功能未启用' })
|
package/lib/tools.js
CHANGED
|
@@ -136,13 +136,13 @@ export function createMilocoTools(options) {
|
|
|
136
136
|
name: 'miloco_device_list',
|
|
137
137
|
description: '列出米家设备(did、名称、在线状态、所属房间、设备类型)。要控制某台设备前,先用本工具拿到 did。',
|
|
138
138
|
parameters: {
|
|
139
|
-
room: { type: 'string', description: '
|
|
139
|
+
room: { type: 'string', description: '按房间名过滤(本地匹配 room_name),留空返回全部设备' },
|
|
140
140
|
},
|
|
141
141
|
async run(args, exec) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
const devices = await call('GET', '/api/miot/device_list', { signal: exec?.signal })
|
|
143
|
+
const room = typeof args.room === 'string' ? args.room.trim().toLowerCase() : ''
|
|
144
|
+
if (!room || !Array.isArray(devices)) return devices
|
|
145
|
+
return devices.filter((device) => String(device?.room_name ?? device?.room ?? '').toLowerCase().includes(room))
|
|
146
146
|
},
|
|
147
147
|
}))
|
|
148
148
|
|
|
@@ -540,6 +540,20 @@ export function createMilocoTools(options) {
|
|
|
540
540
|
|
|
541
541
|
const writeTools = []
|
|
542
542
|
|
|
543
|
+
const ruleActionSchema = {
|
|
544
|
+
type: 'object',
|
|
545
|
+
properties: {
|
|
546
|
+
did: { type: 'string', description: '设备 did;触发场景时填 scene_id' },
|
|
547
|
+
iid: { type: 'string', description: 'prop.{siid}.{piid} / action.{siid}.{aiid} / scene' },
|
|
548
|
+
value: { description: '属性目标值(iid 为 prop.* 时使用)' },
|
|
549
|
+
params: { type: 'array', items: { type: 'string' }, description: '动作参数(iid 为 action.* 时使用)' },
|
|
550
|
+
idempotent: { type: 'boolean', description: '默认 true;false 时必须提供 cooldown_minutes' },
|
|
551
|
+
cooldown_minutes: { type: 'integer', minimum: 0, description: '非幂等动作的去重冷却时间(分钟)' },
|
|
552
|
+
},
|
|
553
|
+
required: ['did', 'iid'],
|
|
554
|
+
additionalProperties: false,
|
|
555
|
+
}
|
|
556
|
+
|
|
543
557
|
writeTools.push(defineMilocoTool({
|
|
544
558
|
name: 'miloco_device_control',
|
|
545
559
|
description: '控制米家设备:设置属性(开关、调温、调亮度、开合窗帘)或调用动作。'
|
|
@@ -549,7 +563,11 @@ export function createMilocoTools(options) {
|
|
|
549
563
|
did: { type: 'string', description: '设备 did' },
|
|
550
564
|
iid: { type: 'string', description: '属性 iid(set_property)或动作 iid(call_action)' },
|
|
551
565
|
value: { type: 'string', description: '目标值;字符串会按属性类型转成布尔/数字' },
|
|
552
|
-
type: {
|
|
566
|
+
type: {
|
|
567
|
+
type: 'string',
|
|
568
|
+
enum: ['set_property', 'set_properties', 'call_action'],
|
|
569
|
+
description: '控制类型:set_property(默认)/ set_properties / call_action',
|
|
570
|
+
},
|
|
553
571
|
properties: {
|
|
554
572
|
type: 'array',
|
|
555
573
|
description: '一次设置多个属性时使用',
|
|
@@ -573,6 +591,9 @@ export function createMilocoTools(options) {
|
|
|
573
591
|
async run(args, exec) {
|
|
574
592
|
const did = requireString(args, 'did')
|
|
575
593
|
const type = args.type ?? 'set_property'
|
|
594
|
+
if (!['set_property', 'set_properties', 'call_action'].includes(type)) {
|
|
595
|
+
throw new Error('type 只能是 set_property / set_properties / call_action')
|
|
596
|
+
}
|
|
576
597
|
const body = { type }
|
|
577
598
|
if (type === 'set_properties') {
|
|
578
599
|
if (!Array.isArray(args.properties) || args.properties.length === 0) {
|
|
@@ -619,46 +640,238 @@ export function createMilocoTools(options) {
|
|
|
619
640
|
|
|
620
641
|
writeTools.push(defineMilocoTool({
|
|
621
642
|
name: 'miloco_rule_create',
|
|
622
|
-
description: '
|
|
623
|
-
+ '
|
|
643
|
+
description: '创建一条结构化自动化规则。condition 指定感知设备与自然语言条件;'
|
|
644
|
+
+ 'event 模式用 actions(设备直控)或 action_descriptions(Agent 回调),'
|
|
645
|
+
+ 'state 模式用 on_enter_* / on_exit_*。task_id 必须是已存在的长期任务。',
|
|
624
646
|
parameters: {
|
|
625
|
-
content: { type: 'string', description: '自然语言规则描述,例如"晚上 22 点客厅灯打开"' },
|
|
626
647
|
name: { type: 'string', description: '规则名称' },
|
|
627
|
-
|
|
628
|
-
|
|
648
|
+
task_id: { type: 'string', description: '关联的任务 id(snake_case,需先创建)' },
|
|
649
|
+
condition: {
|
|
650
|
+
type: 'object',
|
|
651
|
+
description: '触发条件:感知设备列表(OR 语义)+ 自然语言描述',
|
|
652
|
+
properties: {
|
|
653
|
+
perceive_device_ids: {
|
|
654
|
+
type: 'array',
|
|
655
|
+
items: { type: 'string' },
|
|
656
|
+
description: '参与判断的感知设备 did 列表',
|
|
657
|
+
},
|
|
658
|
+
query: { type: 'string', description: '要识别的自然语言条件' },
|
|
659
|
+
},
|
|
660
|
+
required: ['perceive_device_ids', 'query'],
|
|
661
|
+
additionalProperties: false,
|
|
662
|
+
},
|
|
663
|
+
mode: { type: 'string', enum: ['event', 'state'], description: 'event(默认)/ state' },
|
|
664
|
+
direction: {
|
|
665
|
+
type: 'string',
|
|
666
|
+
enum: ['enter', 'exit', 'session', 'milestone', 'guard'],
|
|
667
|
+
description: '可选;提供后优先于 mode,用于表达 exit/session/milestone/guard 语义',
|
|
668
|
+
},
|
|
669
|
+
lifecycle: { type: 'string', enum: ['permanent', 'temporary'], description: '默认 permanent' },
|
|
670
|
+
enabled: { type: 'boolean', description: '是否启用,默认 true' },
|
|
671
|
+
actions: {
|
|
672
|
+
type: 'array',
|
|
673
|
+
items: ruleActionSchema,
|
|
674
|
+
description: 'event 模式的设备直控动作',
|
|
675
|
+
},
|
|
676
|
+
action_descriptions: {
|
|
677
|
+
type: 'array',
|
|
678
|
+
items: { type: 'string' },
|
|
679
|
+
description: 'event 模式下投递给 Agent 的回调提示文本',
|
|
680
|
+
},
|
|
681
|
+
on_enter_actions: {
|
|
682
|
+
type: 'array',
|
|
683
|
+
items: ruleActionSchema,
|
|
684
|
+
description: 'state 模式进入时的设备直控动作',
|
|
685
|
+
},
|
|
686
|
+
on_enter_desc: { type: 'string', description: 'state 模式进入时投递给 Agent 的文本' },
|
|
687
|
+
on_exit_actions: {
|
|
688
|
+
type: 'array',
|
|
689
|
+
items: ruleActionSchema,
|
|
690
|
+
description: 'state 模式退出时的设备直控动作',
|
|
691
|
+
},
|
|
692
|
+
on_exit_desc: { type: 'string', description: 'state 模式退出时投递给 Agent 的文本' },
|
|
693
|
+
on_target_desc: { type: 'string', description: 'duration record 达标瞬间投递的文本' },
|
|
694
|
+
terminate_when: { type: 'string', description: 'lifecycle=temporary 时的自然语言终止条件' },
|
|
695
|
+
exit_debounce_seconds: { type: 'integer', minimum: 0, description: 'state 模式退出防抖秒数' },
|
|
696
|
+
duration_seconds: { type: 'integer', minimum: 1, description: '累计统计窗口(秒)' },
|
|
697
|
+
duration_ratio: { type: 'number', exclusiveMinimum: 0, maximum: 1, description: '窗口内 True 比例阈值' },
|
|
698
|
+
},
|
|
699
|
+
required: ['name', 'task_id', 'condition'],
|
|
629
700
|
async run(args, exec) {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
701
|
+
const name = requireString(args, 'name')
|
|
702
|
+
const taskId = requireString(args, 'task_id')
|
|
703
|
+
if (!/^[a-z0-9_]{1,32}$/.test(taskId)) {
|
|
704
|
+
throw new Error('task_id 必须匹配 [a-z0-9_]{1,32}(只能用小写字母、数字、下划线)')
|
|
705
|
+
}
|
|
706
|
+
const condition = args.condition
|
|
707
|
+
if (!condition || typeof condition !== 'object') throw new Error('缺少参数 condition')
|
|
708
|
+
const deviceIds = Array.isArray(condition.perceive_device_ids)
|
|
709
|
+
? condition.perceive_device_ids.filter(id => typeof id === 'string' && id)
|
|
710
|
+
: []
|
|
711
|
+
const query = typeof condition.query === 'string' ? condition.query.trim() : ''
|
|
712
|
+
if (deviceIds.length === 0) throw new Error('condition.perceive_device_ids 至少需要一个设备 did')
|
|
713
|
+
if (!query) throw new Error('condition.query 不能为空')
|
|
714
|
+
|
|
715
|
+
const modes = ['event', 'state']
|
|
716
|
+
const directions = ['enter', 'exit', 'session', 'milestone', 'guard']
|
|
717
|
+
if (args.direction && !directions.includes(args.direction)) {
|
|
718
|
+
throw new Error(`direction 只能是 ${directions.join(' / ')}`)
|
|
719
|
+
}
|
|
720
|
+
if (args.mode && !modes.includes(args.mode)) {
|
|
721
|
+
throw new Error(`mode 只能是 ${modes.join(' / ')}`)
|
|
722
|
+
}
|
|
723
|
+
if (args.direction && args.mode) {
|
|
724
|
+
const expected = args.direction === 'session' ? 'state' : 'event'
|
|
725
|
+
if (args.mode !== expected) throw new Error(`direction=${args.direction} 对应 mode=${expected}`)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const body = { name, task_id: taskId, condition: { perceive_device_ids: deviceIds, query } }
|
|
729
|
+
if (args.direction) body.direction = args.direction
|
|
730
|
+
else body.mode = args.mode ?? 'event'
|
|
731
|
+
if (args.lifecycle) body.lifecycle = args.lifecycle
|
|
732
|
+
if (args.enabled !== undefined) body.enabled = Boolean(args.enabled)
|
|
733
|
+
for (const key of ['actions', 'action_descriptions', 'on_enter_actions', 'on_exit_actions']) {
|
|
734
|
+
if (args[key] !== undefined) {
|
|
735
|
+
if (!Array.isArray(args[key])) throw new Error(`${key} 必须是数组`)
|
|
736
|
+
body[key] = args[key]
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
for (const key of ['on_enter_desc', 'on_exit_desc', 'on_target_desc', 'terminate_when']) {
|
|
740
|
+
if (args[key] !== undefined) body[key] = args[key]
|
|
741
|
+
}
|
|
742
|
+
for (const key of ['exit_debounce_seconds', 'duration_seconds', 'duration_ratio']) {
|
|
743
|
+
if (args[key] !== undefined) body[key] = args[key]
|
|
744
|
+
}
|
|
635
745
|
return call('POST', '/api/rules', { body, signal: exec?.signal })
|
|
636
746
|
},
|
|
637
747
|
}))
|
|
638
748
|
|
|
749
|
+
writeTools.push(defineMilocoTool({
|
|
750
|
+
name: 'miloco_rule_delete',
|
|
751
|
+
description: '删除一条自动化规则。先用 miloco_rule_list 确认 rule_id;删除后不可恢复。',
|
|
752
|
+
parameters: {
|
|
753
|
+
rule_id: { type: 'string', description: '规则 id' },
|
|
754
|
+
},
|
|
755
|
+
required: ['rule_id'],
|
|
756
|
+
async run(args, exec) {
|
|
757
|
+
const ruleId = requireString(args, 'rule_id')
|
|
758
|
+
return call('DELETE', `/api/rules/${encodeURIComponent(ruleId)}`, { signal: exec?.signal })
|
|
759
|
+
},
|
|
760
|
+
}))
|
|
761
|
+
|
|
639
762
|
writeTools.push(defineMilocoTool({
|
|
640
763
|
name: 'miloco_task_create',
|
|
641
|
-
description: '
|
|
764
|
+
description: '创建一个长期任务的占位行(POST /api/tasks)。'
|
|
765
|
+
+ '任务本身只保存 task_id 与描述;定时提醒再用 miloco_cron_create 关联,'
|
|
766
|
+
+ '条件规则用 miloco_rule_create 关联,累计统计用 miloco_task_record_init 初始化。',
|
|
642
767
|
parameters: {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
768
|
+
task_id: {
|
|
769
|
+
type: 'string',
|
|
770
|
+
pattern: '^[a-z0-9_]{1,32}$',
|
|
771
|
+
description: '任务 id,只能用小写字母、数字、下划线,最长 32 位',
|
|
772
|
+
},
|
|
773
|
+
description: {
|
|
774
|
+
type: 'string',
|
|
775
|
+
maxLength: 200,
|
|
776
|
+
description: '任务描述,最多 200 字',
|
|
777
|
+
},
|
|
778
|
+
lifecycle: {
|
|
779
|
+
type: 'string',
|
|
780
|
+
enum: ['permanent', 'temporary'],
|
|
781
|
+
description: 'permanent(默认)/ temporary',
|
|
782
|
+
},
|
|
783
|
+
expires_at: {
|
|
784
|
+
type: 'string',
|
|
785
|
+
description: '到期时刻,ISO 8601 且带时区;仅 lifecycle=temporary',
|
|
786
|
+
},
|
|
646
787
|
},
|
|
647
|
-
required: ['
|
|
788
|
+
required: ['task_id', 'description'],
|
|
648
789
|
async run(args, exec) {
|
|
649
|
-
const
|
|
650
|
-
if (
|
|
790
|
+
const taskId = requireString(args, 'task_id')
|
|
791
|
+
if (!/^[a-z0-9_]{1,32}$/.test(taskId)) {
|
|
792
|
+
throw new Error('task_id 必须匹配 [a-z0-9_]{1,32}(只能用小写字母、数字、下划线)')
|
|
793
|
+
}
|
|
794
|
+
const description = requireString(args, 'description')
|
|
795
|
+
if (Array.from(description).length > 200) throw new Error('description 不能超过 200 字')
|
|
796
|
+
const body = { task_id: taskId, description }
|
|
797
|
+
if (args.lifecycle !== undefined) {
|
|
798
|
+
if (!['permanent', 'temporary'].includes(args.lifecycle)) {
|
|
799
|
+
throw new Error('lifecycle 只能是 permanent / temporary')
|
|
800
|
+
}
|
|
801
|
+
body.lifecycle = args.lifecycle
|
|
802
|
+
}
|
|
803
|
+
if (args.expires_at !== undefined) {
|
|
804
|
+
if (body.lifecycle !== 'temporary') throw new Error('expires_at 只能配 lifecycle=temporary')
|
|
805
|
+
body.expires_at = requireString(args, 'expires_at')
|
|
806
|
+
}
|
|
651
807
|
return call('POST', '/api/tasks', { body, signal: exec?.signal })
|
|
652
808
|
},
|
|
653
809
|
}))
|
|
654
810
|
|
|
811
|
+
writeTools.push(defineMilocoTool({
|
|
812
|
+
name: 'miloco_task_record_init',
|
|
813
|
+
description: '给已存在的长期任务初始化一份活跃记录(POST /api/tasks/{id}/record)。'
|
|
814
|
+
+ 'kind=progress 需 target/unit/window;kind=duration 可给 target_minutes;kind=event 不需要额外字段。',
|
|
815
|
+
parameters: {
|
|
816
|
+
task_id: { type: 'string', description: '任务 id(snake_case,需先创建)' },
|
|
817
|
+
kind: { type: 'string', enum: ['progress', 'duration', 'event'], description: '记录类型' },
|
|
818
|
+
content: {
|
|
819
|
+
type: 'object',
|
|
820
|
+
description: '按 kind 提供的记录参数',
|
|
821
|
+
properties: {
|
|
822
|
+
target: { type: 'integer', minimum: 1, description: 'progress:目标次数' },
|
|
823
|
+
unit: { type: 'string', description: 'progress:计数单位,例如 次 / 杯' },
|
|
824
|
+
window: {
|
|
825
|
+
type: 'string',
|
|
826
|
+
enum: ['day', 'week', 'month', 'longterm'],
|
|
827
|
+
description: 'progress:统计窗口',
|
|
828
|
+
},
|
|
829
|
+
target_minutes: { type: 'integer', minimum: 1, description: 'duration:每日目标分钟数' },
|
|
830
|
+
recurring_pattern: { type: 'object', additionalProperties: true, description: '可选重复规则' },
|
|
831
|
+
expires_at: { type: 'string', description: '可选到期时刻(ISO 8601)' },
|
|
832
|
+
},
|
|
833
|
+
additionalProperties: false,
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
required: ['task_id', 'kind'],
|
|
837
|
+
async run(args, exec) {
|
|
838
|
+
const taskId = requireString(args, 'task_id')
|
|
839
|
+
if (!/^[a-z0-9_]{1,32}$/.test(taskId)) {
|
|
840
|
+
throw new Error('task_id 必须匹配 [a-z0-9_]{1,32}(只能用小写字母、数字、下划线)')
|
|
841
|
+
}
|
|
842
|
+
const kind = requireString(args, 'kind')
|
|
843
|
+
if (!['progress', 'duration', 'event'].includes(kind)) {
|
|
844
|
+
throw new Error('kind 只能是 progress / duration / event')
|
|
845
|
+
}
|
|
846
|
+
const content = args.content && typeof args.content === 'object' ? args.content : {}
|
|
847
|
+
if (kind === 'progress') {
|
|
848
|
+
if (!Number.isInteger(content.target) || content.target <= 0) {
|
|
849
|
+
throw new Error('progress 需要正整数 content.target')
|
|
850
|
+
}
|
|
851
|
+
if (!content.unit) throw new Error('progress 需要 content.unit')
|
|
852
|
+
if (!['day', 'week', 'month', 'longterm'].includes(content.window)) {
|
|
853
|
+
throw new Error('progress 需要 content.window:day / week / month / longterm')
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (kind === 'duration' && content.target_minutes !== undefined) {
|
|
857
|
+
if (!Number.isInteger(content.target_minutes) || content.target_minutes <= 0) {
|
|
858
|
+
throw new Error('content.target_minutes 必须是正整数')
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return call('POST', `/api/tasks/${encodeURIComponent(taskId)}/record`, {
|
|
862
|
+
body: { kind, content },
|
|
863
|
+
signal: exec?.signal,
|
|
864
|
+
})
|
|
865
|
+
},
|
|
866
|
+
}))
|
|
867
|
+
|
|
655
868
|
writeTools.push(defineMilocoTool({
|
|
656
869
|
name: 'miloco_task_delete',
|
|
657
870
|
description: '终止并删除一个长期任务及其关联的规则/定时计划/统计记录。'
|
|
658
871
|
+ 'reason=completed/expired/abandoned;用户主动取消时必须先二次确认。',
|
|
659
872
|
parameters: {
|
|
660
873
|
task_id: { type: 'string', description: '任务 id' },
|
|
661
|
-
reason: { type: 'string', description: 'completed / expired / abandoned' },
|
|
874
|
+
reason: { type: 'string', enum: ['completed', 'expired', 'abandoned'], description: 'completed / expired / abandoned' },
|
|
662
875
|
},
|
|
663
876
|
required: ['task_id', 'reason'],
|
|
664
877
|
async run(args, exec) {
|
|
@@ -679,18 +892,23 @@ export function createMilocoTools(options) {
|
|
|
679
892
|
description: '在后端注册一个定时计划(cron 表达式 / 指定时刻 / 固定间隔),到点后由 Miloco 主动把事件投递给 DSH。',
|
|
680
893
|
parameters: {
|
|
681
894
|
name: { type: 'string', description: '计划名称' },
|
|
682
|
-
kind: { type: 'string', description: 'cron / at / every' },
|
|
895
|
+
kind: { type: 'string', enum: ['cron', 'at', 'every'], description: 'cron / at / every' },
|
|
683
896
|
cron_expr: { type: 'string', description: 'cron 表达式,如 "0 22 * * *"' },
|
|
684
897
|
at_iso: { type: 'string', description: 'ISO 8601 时刻(kind=at)' },
|
|
685
|
-
every_ms: { type: 'integer', description: '
|
|
898
|
+
every_ms: { type: 'integer', minimum: 60000, description: '间隔毫秒,最小 60000(kind=every)' },
|
|
686
899
|
tz: { type: 'string', description: '时区,如 Asia/Shanghai' },
|
|
687
900
|
message: { type: 'string', description: '到点后投递给 Agent 的文本' },
|
|
688
901
|
task_id: { type: 'string', description: '关联的长期任务 id' },
|
|
689
902
|
},
|
|
690
903
|
required: ['kind', 'message'],
|
|
691
904
|
async run(args, exec) {
|
|
905
|
+
const kind = requireString(args, 'kind')
|
|
906
|
+
if (!['cron', 'at', 'every'].includes(kind)) throw new Error('kind 只能是 cron / at / every')
|
|
907
|
+
if (kind === 'cron' && !args.cron_expr) throw new Error('kind=cron 需要 cron_expr')
|
|
908
|
+
if (kind === 'at' && !args.at_iso) throw new Error('kind=at 需要 at_iso')
|
|
909
|
+
if (kind === 'every' && !args.every_ms) throw new Error('kind=every 需要 every_ms')
|
|
692
910
|
const body = {
|
|
693
|
-
kind
|
|
911
|
+
kind,
|
|
694
912
|
message: requireString(args, 'message'),
|
|
695
913
|
name: args.name ?? 'miloco-cron',
|
|
696
914
|
cron_expr: args.cron_expr,
|
|
@@ -703,6 +921,19 @@ export function createMilocoTools(options) {
|
|
|
703
921
|
},
|
|
704
922
|
}))
|
|
705
923
|
|
|
924
|
+
writeTools.push(defineMilocoTool({
|
|
925
|
+
name: 'miloco_cron_delete',
|
|
926
|
+
description: '删除一个定时计划(POST /api/crons 创建的 cron/at/every)。先用 miloco_cron_list 确认 cron_id。',
|
|
927
|
+
parameters: {
|
|
928
|
+
cron_id: { type: 'string', description: '定时计划 id' },
|
|
929
|
+
},
|
|
930
|
+
required: ['cron_id'],
|
|
931
|
+
async run(args, exec) {
|
|
932
|
+
const cronId = requireString(args, 'cron_id')
|
|
933
|
+
return call('DELETE', `/api/crons/${encodeURIComponent(cronId)}`, { signal: exec?.signal })
|
|
934
|
+
},
|
|
935
|
+
}))
|
|
936
|
+
|
|
706
937
|
writeTools.push(defineMilocoTool({
|
|
707
938
|
name: 'miloco_scope_set',
|
|
708
939
|
description: '设置参与感知的摄像头(in_use=true 开始观察,false 停止观察)。这是隐私相关操作,改动前请向用户确认。',
|
|
@@ -744,7 +975,11 @@ export function createMilocoTools(options) {
|
|
|
744
975
|
items: {
|
|
745
976
|
type: 'object',
|
|
746
977
|
properties: {
|
|
747
|
-
op: {
|
|
978
|
+
op: {
|
|
979
|
+
type: 'string',
|
|
980
|
+
enum: ['add', 'merge', 'replace', 'update', 'delete'],
|
|
981
|
+
description: 'add / merge / replace / update / delete',
|
|
982
|
+
},
|
|
748
983
|
id: { type: 'string', description: '正式区条目 id(merge/replace/update/delete 用)' },
|
|
749
984
|
from: { type: 'string', description: '候选条目 id(带上即消费该候选)' },
|
|
750
985
|
entry: {
|
|
@@ -753,13 +988,17 @@ export function createMilocoTools(options) {
|
|
|
753
988
|
properties: {
|
|
754
989
|
type: {
|
|
755
990
|
type: 'string',
|
|
756
|
-
|
|
991
|
+
enum: [
|
|
992
|
+
'member_persona', 'member_health', 'member_routine', 'member_entertain',
|
|
993
|
+
'member_preference', 'family', 'space', 'device',
|
|
994
|
+
],
|
|
995
|
+
description: '成员/家庭/空间/设备档案类型',
|
|
757
996
|
},
|
|
758
997
|
subject_id: { type: 'string', description: '成员 person_id' },
|
|
759
998
|
subject_name: { type: 'string', description: '主体名称(成员称谓 / 空间名)' },
|
|
760
999
|
content: { type: 'string', description: '条目正文' },
|
|
761
|
-
confidence: { type: 'number', description: '置信度 0~1' },
|
|
762
|
-
source: { type: 'string', description: 'observed / user_told' },
|
|
1000
|
+
confidence: { type: 'number', minimum: 0, maximum: 1, description: '置信度 0~1' },
|
|
1001
|
+
source: { type: 'string', enum: ['observed', 'user_told'], description: 'observed / user_told' },
|
|
763
1002
|
evidence_log: { type: 'array', items: { type: 'string' }, description: '证据' },
|
|
764
1003
|
},
|
|
765
1004
|
required: ['type', 'content'],
|
|
@@ -770,12 +1009,18 @@ export function createMilocoTools(options) {
|
|
|
770
1009
|
additionalProperties: true,
|
|
771
1010
|
},
|
|
772
1011
|
},
|
|
1012
|
+
user_edit: {
|
|
1013
|
+
type: 'boolean',
|
|
1014
|
+
description: '用户直接告知的内容设为 true(后端会按 user_told / 高置信度处理)',
|
|
1015
|
+
},
|
|
773
1016
|
},
|
|
774
1017
|
required: ['ops'],
|
|
775
1018
|
async run(args, exec) {
|
|
776
1019
|
const ops = Array.isArray(args.ops) ? args.ops : []
|
|
777
1020
|
if (ops.length === 0) throw new Error('缺少参数 ops')
|
|
778
|
-
|
|
1021
|
+
const body = { ops }
|
|
1022
|
+
if (args.user_edit !== undefined) body.user_edit = Boolean(args.user_edit)
|
|
1023
|
+
return call('POST', '/api/home-profile/profile:write', { body, signal: exec?.signal })
|
|
779
1024
|
},
|
|
780
1025
|
}))
|
|
781
1026
|
|
|
@@ -789,29 +1034,37 @@ export function createMilocoTools(options) {
|
|
|
789
1034
|
items: {
|
|
790
1035
|
type: 'object',
|
|
791
1036
|
properties: {
|
|
792
|
-
op: {
|
|
1037
|
+
op: {
|
|
1038
|
+
type: 'string',
|
|
1039
|
+
enum: ['add', 'merge', 'update', 'delete'],
|
|
1040
|
+
description: 'add / merge / update / delete',
|
|
1041
|
+
},
|
|
793
1042
|
id: { type: 'string', description: '候选条目 id' },
|
|
794
|
-
date: { type: 'string', description: '
|
|
1043
|
+
date: { type: 'string', description: '观察日期(YYYY-MM-DD)' },
|
|
795
1044
|
entry: {
|
|
796
1045
|
type: 'object',
|
|
797
1046
|
description: '条目内容',
|
|
798
1047
|
properties: {
|
|
799
1048
|
type: {
|
|
800
1049
|
type: 'string',
|
|
801
|
-
|
|
1050
|
+
enum: [
|
|
1051
|
+
'member_persona', 'member_health', 'member_routine', 'member_entertain',
|
|
1052
|
+
'member_preference', 'family', 'space', 'device',
|
|
1053
|
+
],
|
|
1054
|
+
description: '成员/家庭/空间/设备档案类型',
|
|
802
1055
|
},
|
|
803
1056
|
subject_id: { type: 'string', description: '成员 person_id' },
|
|
804
1057
|
subject_name: { type: 'string', description: '主体名称' },
|
|
805
1058
|
content: { type: 'string', description: '条目正文' },
|
|
806
|
-
confidence: { type: 'number', description: '置信度 0~1' },
|
|
807
|
-
source: { type: 'string', description: 'observed / user_told' },
|
|
1059
|
+
confidence: { type: 'number', minimum: 0, maximum: 1, description: '置信度 0~1' },
|
|
1060
|
+
source: { type: 'string', enum: ['observed', 'user_told'], description: 'observed / user_told' },
|
|
808
1061
|
evidence_log: { type: 'array', items: { type: 'string' }, description: '证据' },
|
|
809
1062
|
},
|
|
810
1063
|
required: ['type', 'content'],
|
|
811
1064
|
additionalProperties: true,
|
|
812
1065
|
},
|
|
813
1066
|
},
|
|
814
|
-
required: ['op'],
|
|
1067
|
+
required: ['op', 'date'],
|
|
815
1068
|
additionalProperties: true,
|
|
816
1069
|
},
|
|
817
1070
|
},
|
|
@@ -820,6 +1073,9 @@ export function createMilocoTools(options) {
|
|
|
820
1073
|
async run(args, exec) {
|
|
821
1074
|
const ops = Array.isArray(args.ops) ? args.ops : []
|
|
822
1075
|
if (ops.length === 0) throw new Error('缺少参数 ops')
|
|
1076
|
+
if (ops.some(op => !op || typeof op.date !== 'string' || !op.date.trim())) {
|
|
1077
|
+
throw new Error('candidates:write 的每个 op 都必须带 date(YYYY-MM-DD)')
|
|
1078
|
+
}
|
|
823
1079
|
return call('POST', '/api/home-profile/candidates:write', { body: { ops }, signal: exec?.signal })
|
|
824
1080
|
},
|
|
825
1081
|
}))
|
|
@@ -828,7 +1084,7 @@ export function createMilocoTools(options) {
|
|
|
828
1084
|
name: 'miloco_home_profile_commit',
|
|
829
1085
|
description: '提交家庭档案:后端会自动做过期候选清理(30 天且证据不足 3 条)、token 预算归档与 markdown 渲染持久化。',
|
|
830
1086
|
async run(_args, exec) {
|
|
831
|
-
return call('POST', '/api/home-profile/commit', {
|
|
1087
|
+
return call('POST', '/api/home-profile/commit', { signal: exec?.signal })
|
|
832
1088
|
},
|
|
833
1089
|
}))
|
|
834
1090
|
|
|
@@ -1032,14 +1288,22 @@ export function createMilocoTools(options) {
|
|
|
1032
1288
|
description: '本地图片路径列表',
|
|
1033
1289
|
items: { type: 'string' },
|
|
1034
1290
|
},
|
|
1291
|
+
mode: {
|
|
1292
|
+
type: 'string',
|
|
1293
|
+
enum: ['replace', 'append'],
|
|
1294
|
+
description: 'replace(整组替换,默认)/ append(按质量分保留 top-3)',
|
|
1295
|
+
},
|
|
1035
1296
|
},
|
|
1036
1297
|
required: ['pet_id', 'files'],
|
|
1037
1298
|
async run(args, exec) {
|
|
1038
1299
|
const petId = requireString(args, 'pet_id')
|
|
1039
1300
|
const paths = Array.isArray(args.files) ? args.files.filter(f => typeof f === 'string' && f) : []
|
|
1040
1301
|
if (paths.length === 0) throw new Error('缺少参数 files')
|
|
1302
|
+
const fields = {}
|
|
1303
|
+
if (args.mode !== undefined) fields.mode = args.mode
|
|
1041
1304
|
return client.postMultipart(`/api/identity/pets/${encodeURIComponent(petId)}/reference-crops`, {
|
|
1042
|
-
files: paths.map(p => ({ field: '
|
|
1305
|
+
files: paths.map(p => ({ field: 'crops', path: p })),
|
|
1306
|
+
fields,
|
|
1043
1307
|
signal: exec?.signal,
|
|
1044
1308
|
})
|
|
1045
1309
|
},
|
|
@@ -1141,9 +1405,12 @@ export const WRITE_TOOL_NAMES = [
|
|
|
1141
1405
|
'miloco_scene_trigger',
|
|
1142
1406
|
'miloco_notify_push',
|
|
1143
1407
|
'miloco_rule_create',
|
|
1408
|
+
'miloco_rule_delete',
|
|
1144
1409
|
'miloco_task_create',
|
|
1410
|
+
'miloco_task_record_init',
|
|
1145
1411
|
'miloco_task_delete',
|
|
1146
1412
|
'miloco_cron_create',
|
|
1413
|
+
'miloco_cron_delete',
|
|
1147
1414
|
'miloco_scope_set',
|
|
1148
1415
|
'miloco_home_profile_write',
|
|
1149
1416
|
'miloco_home_profile_candidate_write',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-miloco-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Unofficial DSH bridge for Xiaomi Miloco: an isolated Agent preset for Miloco tools, native tools/skills, Web settings, perception-priority filtering, and Miloco -> DSH push.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"lib",
|
|
31
31
|
"skills",
|
|
32
32
|
"adapter",
|
|
33
|
-
"
|
|
33
|
+
"docs",
|
|
34
34
|
"scripts",
|
|
35
35
|
"install.sh",
|
|
36
36
|
"cordis.patch.yml",
|