dsh-miloco-bridge 0.1.0 → 0.1.2
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 +80 -5
- package/{doc → docs}/ARCHITECTURE.md +13 -6
- package/{doc → docs}/INSTALL.md +25 -11
- 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 +693 -119
- package/lib/deploy.js +236 -38
- package/lib/github-release.js +162 -0
- package/lib/index.js +11 -1
- package/lib/settings-api.js +170 -1
- package/lib/tools.js +365 -40
- package/package.json +2 -2
- package/skills/miloco-create-task/SKILL.md +63 -14
- package/skills/miloco-devices/SKILL.md +3 -2
- package/doc/images/agent-presets.png +0 -0
- package/doc/images/miloco-settings.png +0 -0
package/lib/tools.js
CHANGED
|
@@ -23,6 +23,16 @@ import { MilocoError } from './miloco-client.js'
|
|
|
23
23
|
import { PERCEPTION_LEVELS } from './perception.js'
|
|
24
24
|
import { noteSession } from './state.js'
|
|
25
25
|
|
|
26
|
+
/** Extra device fields that callers may request explicitly. */
|
|
27
|
+
const DEVICE_LIST_EXTRA_FIELDS = [
|
|
28
|
+
'icon',
|
|
29
|
+
'local_ip',
|
|
30
|
+
'home_name',
|
|
31
|
+
'order_time',
|
|
32
|
+
'lan_online',
|
|
33
|
+
'sub_devices',
|
|
34
|
+
]
|
|
35
|
+
|
|
26
36
|
/**
|
|
27
37
|
* Serialise a canonical tool value as readable JSON text.
|
|
28
38
|
* @param {unknown} value - Value to serialise.
|
|
@@ -134,15 +144,63 @@ export function createMilocoTools(options) {
|
|
|
134
144
|
|
|
135
145
|
tools.push(defineMilocoTool({
|
|
136
146
|
name: 'miloco_device_list',
|
|
137
|
-
description: '
|
|
147
|
+
description: '列出米家设备。默认返回 did、名称、在线状态、房间、型号和 home_id;'
|
|
148
|
+
+ '可用 include 显式取回 icon/local_ip 等额外字段。默认最多 500 台,超过时用 offset 翻页。',
|
|
138
149
|
parameters: {
|
|
139
|
-
room: { type: 'string', description: '
|
|
150
|
+
room: { type: 'string', description: '按房间名过滤(本地匹配 room_name)' },
|
|
151
|
+
name: { type: 'string', description: '按设备名过滤(本地子串匹配,忽略大小写)' },
|
|
152
|
+
online_only: { type: 'boolean', description: '只返回在线设备' },
|
|
153
|
+
limit: {
|
|
154
|
+
type: 'integer',
|
|
155
|
+
minimum: 1,
|
|
156
|
+
maximum: 500,
|
|
157
|
+
description: '最多返回多少台,默认 500',
|
|
158
|
+
},
|
|
159
|
+
offset: { type: 'integer', minimum: 0, description: '从第几台开始,默认 0' },
|
|
160
|
+
include: {
|
|
161
|
+
type: 'array',
|
|
162
|
+
items: { type: 'string', enum: DEVICE_LIST_EXTRA_FIELDS },
|
|
163
|
+
description: '额外字段:icon / local_ip / home_name / order_time / lan_online / sub_devices',
|
|
164
|
+
},
|
|
140
165
|
},
|
|
141
166
|
async run(args, exec) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
const devices = await call('GET', '/api/miot/device_list', { signal: exec?.signal })
|
|
168
|
+
const list = Array.isArray(devices) ? devices : []
|
|
169
|
+
const room = typeof args.room === 'string' ? args.room.trim().toLowerCase() : ''
|
|
170
|
+
const name = typeof args.name === 'string' ? args.name.trim().toLowerCase() : ''
|
|
171
|
+
const onlineOnly = args.online_only === true
|
|
172
|
+
const includes = Array.isArray(args.include)
|
|
173
|
+
? [...new Set(args.include.filter(field => typeof field === 'string'))]
|
|
174
|
+
: []
|
|
175
|
+
for (const field of includes) {
|
|
176
|
+
if (!DEVICE_LIST_EXTRA_FIELDS.includes(field)) throw new Error(`include 不支持字段:${field}`)
|
|
177
|
+
}
|
|
178
|
+
const limit = args.limit === undefined ? 500 : Number(args.limit)
|
|
179
|
+
const offset = args.offset === undefined ? 0 : Number(args.offset)
|
|
180
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error('limit 必须是 1~500 的整数')
|
|
181
|
+
if (!Number.isInteger(offset) || offset < 0) throw new Error('offset 必须是非负整数')
|
|
182
|
+
const filtered = list.filter((device) => {
|
|
183
|
+
if (!device || typeof device !== 'object') return false
|
|
184
|
+
if (onlineOnly && device.online !== true) return false
|
|
185
|
+
if (room && !String(device.room_name ?? device.room ?? '').toLowerCase().includes(room)) return false
|
|
186
|
+
if (name && !String(device.name ?? '').toLowerCase().includes(name)) return false
|
|
187
|
+
return true
|
|
145
188
|
})
|
|
189
|
+
const page = filtered.slice(offset, offset + limit).map((device) => {
|
|
190
|
+
const row = {
|
|
191
|
+
did: device.did ?? '',
|
|
192
|
+
name: device.name ?? '',
|
|
193
|
+
online: device.online === true,
|
|
194
|
+
room_name: device.room_name ?? '',
|
|
195
|
+
model: device.model ?? '',
|
|
196
|
+
home_id: device.home_id ?? '',
|
|
197
|
+
}
|
|
198
|
+
for (const field of includes) {
|
|
199
|
+
if (Object.prototype.hasOwnProperty.call(device, field)) row[field] = device[field]
|
|
200
|
+
}
|
|
201
|
+
return row
|
|
202
|
+
})
|
|
203
|
+
return { total: filtered.length, returned: page.length, offset, limit, devices: page }
|
|
146
204
|
},
|
|
147
205
|
}))
|
|
148
206
|
|
|
@@ -540,6 +598,20 @@ export function createMilocoTools(options) {
|
|
|
540
598
|
|
|
541
599
|
const writeTools = []
|
|
542
600
|
|
|
601
|
+
const ruleActionSchema = {
|
|
602
|
+
type: 'object',
|
|
603
|
+
properties: {
|
|
604
|
+
did: { type: 'string', description: '设备 did;触发场景时填 scene_id' },
|
|
605
|
+
iid: { type: 'string', description: 'prop.{siid}.{piid} / action.{siid}.{aiid} / scene' },
|
|
606
|
+
value: { description: '属性目标值(iid 为 prop.* 时使用)' },
|
|
607
|
+
params: { type: 'array', items: { type: 'string' }, description: '动作参数(iid 为 action.* 时使用)' },
|
|
608
|
+
idempotent: { type: 'boolean', description: '默认 true;false 时必须提供 cooldown_minutes' },
|
|
609
|
+
cooldown_minutes: { type: 'integer', minimum: 0, description: '非幂等动作的去重冷却时间(分钟)' },
|
|
610
|
+
},
|
|
611
|
+
required: ['did', 'iid'],
|
|
612
|
+
additionalProperties: false,
|
|
613
|
+
}
|
|
614
|
+
|
|
543
615
|
writeTools.push(defineMilocoTool({
|
|
544
616
|
name: 'miloco_device_control',
|
|
545
617
|
description: '控制米家设备:设置属性(开关、调温、调亮度、开合窗帘)或调用动作。'
|
|
@@ -549,7 +621,11 @@ export function createMilocoTools(options) {
|
|
|
549
621
|
did: { type: 'string', description: '设备 did' },
|
|
550
622
|
iid: { type: 'string', description: '属性 iid(set_property)或动作 iid(call_action)' },
|
|
551
623
|
value: { type: 'string', description: '目标值;字符串会按属性类型转成布尔/数字' },
|
|
552
|
-
type: {
|
|
624
|
+
type: {
|
|
625
|
+
type: 'string',
|
|
626
|
+
enum: ['set_property', 'set_properties', 'call_action'],
|
|
627
|
+
description: '控制类型:set_property(默认)/ set_properties / call_action',
|
|
628
|
+
},
|
|
553
629
|
properties: {
|
|
554
630
|
type: 'array',
|
|
555
631
|
description: '一次设置多个属性时使用',
|
|
@@ -573,6 +649,9 @@ export function createMilocoTools(options) {
|
|
|
573
649
|
async run(args, exec) {
|
|
574
650
|
const did = requireString(args, 'did')
|
|
575
651
|
const type = args.type ?? 'set_property'
|
|
652
|
+
if (!['set_property', 'set_properties', 'call_action'].includes(type)) {
|
|
653
|
+
throw new Error('type 只能是 set_property / set_properties / call_action')
|
|
654
|
+
}
|
|
576
655
|
const body = { type }
|
|
577
656
|
if (type === 'set_properties') {
|
|
578
657
|
if (!Array.isArray(args.properties) || args.properties.length === 0) {
|
|
@@ -619,46 +698,238 @@ export function createMilocoTools(options) {
|
|
|
619
698
|
|
|
620
699
|
writeTools.push(defineMilocoTool({
|
|
621
700
|
name: 'miloco_rule_create',
|
|
622
|
-
description: '
|
|
623
|
-
+ '
|
|
701
|
+
description: '创建一条结构化自动化规则。condition 指定感知设备与自然语言条件;'
|
|
702
|
+
+ 'event 模式用 actions(设备直控)或 action_descriptions(Agent 回调),'
|
|
703
|
+
+ 'state 模式用 on_enter_* / on_exit_*。task_id 必须是已存在的长期任务。',
|
|
624
704
|
parameters: {
|
|
625
|
-
content: { type: 'string', description: '自然语言规则描述,例如"晚上 22 点客厅灯打开"' },
|
|
626
705
|
name: { type: 'string', description: '规则名称' },
|
|
627
|
-
|
|
628
|
-
|
|
706
|
+
task_id: { type: 'string', description: '关联的任务 id(snake_case,需先创建)' },
|
|
707
|
+
condition: {
|
|
708
|
+
type: 'object',
|
|
709
|
+
description: '触发条件:感知设备列表(OR 语义)+ 自然语言描述',
|
|
710
|
+
properties: {
|
|
711
|
+
perceive_device_ids: {
|
|
712
|
+
type: 'array',
|
|
713
|
+
items: { type: 'string' },
|
|
714
|
+
description: '参与判断的感知设备 did 列表',
|
|
715
|
+
},
|
|
716
|
+
query: { type: 'string', description: '要识别的自然语言条件' },
|
|
717
|
+
},
|
|
718
|
+
required: ['perceive_device_ids', 'query'],
|
|
719
|
+
additionalProperties: false,
|
|
720
|
+
},
|
|
721
|
+
mode: { type: 'string', enum: ['event', 'state'], description: 'event(默认)/ state' },
|
|
722
|
+
direction: {
|
|
723
|
+
type: 'string',
|
|
724
|
+
enum: ['enter', 'exit', 'session', 'milestone', 'guard'],
|
|
725
|
+
description: '可选;提供后优先于 mode,用于表达 exit/session/milestone/guard 语义',
|
|
726
|
+
},
|
|
727
|
+
lifecycle: { type: 'string', enum: ['permanent', 'temporary'], description: '默认 permanent' },
|
|
728
|
+
enabled: { type: 'boolean', description: '是否启用,默认 true' },
|
|
729
|
+
actions: {
|
|
730
|
+
type: 'array',
|
|
731
|
+
items: ruleActionSchema,
|
|
732
|
+
description: 'event 模式的设备直控动作',
|
|
733
|
+
},
|
|
734
|
+
action_descriptions: {
|
|
735
|
+
type: 'array',
|
|
736
|
+
items: { type: 'string' },
|
|
737
|
+
description: 'event 模式下投递给 Agent 的回调提示文本',
|
|
738
|
+
},
|
|
739
|
+
on_enter_actions: {
|
|
740
|
+
type: 'array',
|
|
741
|
+
items: ruleActionSchema,
|
|
742
|
+
description: 'state 模式进入时的设备直控动作',
|
|
743
|
+
},
|
|
744
|
+
on_enter_desc: { type: 'string', description: 'state 模式进入时投递给 Agent 的文本' },
|
|
745
|
+
on_exit_actions: {
|
|
746
|
+
type: 'array',
|
|
747
|
+
items: ruleActionSchema,
|
|
748
|
+
description: 'state 模式退出时的设备直控动作',
|
|
749
|
+
},
|
|
750
|
+
on_exit_desc: { type: 'string', description: 'state 模式退出时投递给 Agent 的文本' },
|
|
751
|
+
on_target_desc: { type: 'string', description: 'duration record 达标瞬间投递的文本' },
|
|
752
|
+
terminate_when: { type: 'string', description: 'lifecycle=temporary 时的自然语言终止条件' },
|
|
753
|
+
exit_debounce_seconds: { type: 'integer', minimum: 0, description: 'state 模式退出防抖秒数' },
|
|
754
|
+
duration_seconds: { type: 'integer', minimum: 1, description: '累计统计窗口(秒)' },
|
|
755
|
+
duration_ratio: { type: 'number', exclusiveMinimum: 0, maximum: 1, description: '窗口内 True 比例阈值' },
|
|
756
|
+
},
|
|
757
|
+
required: ['name', 'task_id', 'condition'],
|
|
629
758
|
async run(args, exec) {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
759
|
+
const name = requireString(args, 'name')
|
|
760
|
+
const taskId = requireString(args, 'task_id')
|
|
761
|
+
if (!/^[a-z0-9_]{1,32}$/.test(taskId)) {
|
|
762
|
+
throw new Error('task_id 必须匹配 [a-z0-9_]{1,32}(只能用小写字母、数字、下划线)')
|
|
763
|
+
}
|
|
764
|
+
const condition = args.condition
|
|
765
|
+
if (!condition || typeof condition !== 'object') throw new Error('缺少参数 condition')
|
|
766
|
+
const deviceIds = Array.isArray(condition.perceive_device_ids)
|
|
767
|
+
? condition.perceive_device_ids.filter(id => typeof id === 'string' && id)
|
|
768
|
+
: []
|
|
769
|
+
const query = typeof condition.query === 'string' ? condition.query.trim() : ''
|
|
770
|
+
if (deviceIds.length === 0) throw new Error('condition.perceive_device_ids 至少需要一个设备 did')
|
|
771
|
+
if (!query) throw new Error('condition.query 不能为空')
|
|
772
|
+
|
|
773
|
+
const modes = ['event', 'state']
|
|
774
|
+
const directions = ['enter', 'exit', 'session', 'milestone', 'guard']
|
|
775
|
+
if (args.direction && !directions.includes(args.direction)) {
|
|
776
|
+
throw new Error(`direction 只能是 ${directions.join(' / ')}`)
|
|
777
|
+
}
|
|
778
|
+
if (args.mode && !modes.includes(args.mode)) {
|
|
779
|
+
throw new Error(`mode 只能是 ${modes.join(' / ')}`)
|
|
780
|
+
}
|
|
781
|
+
if (args.direction && args.mode) {
|
|
782
|
+
const expected = args.direction === 'session' ? 'state' : 'event'
|
|
783
|
+
if (args.mode !== expected) throw new Error(`direction=${args.direction} 对应 mode=${expected}`)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const body = { name, task_id: taskId, condition: { perceive_device_ids: deviceIds, query } }
|
|
787
|
+
if (args.direction) body.direction = args.direction
|
|
788
|
+
else body.mode = args.mode ?? 'event'
|
|
789
|
+
if (args.lifecycle) body.lifecycle = args.lifecycle
|
|
790
|
+
if (args.enabled !== undefined) body.enabled = Boolean(args.enabled)
|
|
791
|
+
for (const key of ['actions', 'action_descriptions', 'on_enter_actions', 'on_exit_actions']) {
|
|
792
|
+
if (args[key] !== undefined) {
|
|
793
|
+
if (!Array.isArray(args[key])) throw new Error(`${key} 必须是数组`)
|
|
794
|
+
body[key] = args[key]
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
for (const key of ['on_enter_desc', 'on_exit_desc', 'on_target_desc', 'terminate_when']) {
|
|
798
|
+
if (args[key] !== undefined) body[key] = args[key]
|
|
799
|
+
}
|
|
800
|
+
for (const key of ['exit_debounce_seconds', 'duration_seconds', 'duration_ratio']) {
|
|
801
|
+
if (args[key] !== undefined) body[key] = args[key]
|
|
802
|
+
}
|
|
635
803
|
return call('POST', '/api/rules', { body, signal: exec?.signal })
|
|
636
804
|
},
|
|
637
805
|
}))
|
|
638
806
|
|
|
807
|
+
writeTools.push(defineMilocoTool({
|
|
808
|
+
name: 'miloco_rule_delete',
|
|
809
|
+
description: '删除一条自动化规则。先用 miloco_rule_list 确认 rule_id;删除后不可恢复。',
|
|
810
|
+
parameters: {
|
|
811
|
+
rule_id: { type: 'string', description: '规则 id' },
|
|
812
|
+
},
|
|
813
|
+
required: ['rule_id'],
|
|
814
|
+
async run(args, exec) {
|
|
815
|
+
const ruleId = requireString(args, 'rule_id')
|
|
816
|
+
return call('DELETE', `/api/rules/${encodeURIComponent(ruleId)}`, { signal: exec?.signal })
|
|
817
|
+
},
|
|
818
|
+
}))
|
|
819
|
+
|
|
639
820
|
writeTools.push(defineMilocoTool({
|
|
640
821
|
name: 'miloco_task_create',
|
|
641
|
-
description: '
|
|
822
|
+
description: '创建一个长期任务的占位行(POST /api/tasks)。'
|
|
823
|
+
+ '任务本身只保存 task_id 与描述;定时提醒再用 miloco_cron_create 关联,'
|
|
824
|
+
+ '条件规则用 miloco_rule_create 关联,累计统计用 miloco_task_record_init 初始化。',
|
|
642
825
|
parameters: {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
826
|
+
task_id: {
|
|
827
|
+
type: 'string',
|
|
828
|
+
pattern: '^[a-z0-9_]{1,32}$',
|
|
829
|
+
description: '任务 id,只能用小写字母、数字、下划线,最长 32 位',
|
|
830
|
+
},
|
|
831
|
+
description: {
|
|
832
|
+
type: 'string',
|
|
833
|
+
maxLength: 200,
|
|
834
|
+
description: '任务描述,最多 200 字',
|
|
835
|
+
},
|
|
836
|
+
lifecycle: {
|
|
837
|
+
type: 'string',
|
|
838
|
+
enum: ['permanent', 'temporary'],
|
|
839
|
+
description: 'permanent(默认)/ temporary',
|
|
840
|
+
},
|
|
841
|
+
expires_at: {
|
|
842
|
+
type: 'string',
|
|
843
|
+
description: '到期时刻,ISO 8601 且带时区;仅 lifecycle=temporary',
|
|
844
|
+
},
|
|
646
845
|
},
|
|
647
|
-
required: ['
|
|
846
|
+
required: ['task_id', 'description'],
|
|
648
847
|
async run(args, exec) {
|
|
649
|
-
const
|
|
650
|
-
if (
|
|
848
|
+
const taskId = requireString(args, 'task_id')
|
|
849
|
+
if (!/^[a-z0-9_]{1,32}$/.test(taskId)) {
|
|
850
|
+
throw new Error('task_id 必须匹配 [a-z0-9_]{1,32}(只能用小写字母、数字、下划线)')
|
|
851
|
+
}
|
|
852
|
+
const description = requireString(args, 'description')
|
|
853
|
+
if (Array.from(description).length > 200) throw new Error('description 不能超过 200 字')
|
|
854
|
+
const body = { task_id: taskId, description }
|
|
855
|
+
if (args.lifecycle !== undefined) {
|
|
856
|
+
if (!['permanent', 'temporary'].includes(args.lifecycle)) {
|
|
857
|
+
throw new Error('lifecycle 只能是 permanent / temporary')
|
|
858
|
+
}
|
|
859
|
+
body.lifecycle = args.lifecycle
|
|
860
|
+
}
|
|
861
|
+
if (args.expires_at !== undefined) {
|
|
862
|
+
if (body.lifecycle !== 'temporary') throw new Error('expires_at 只能配 lifecycle=temporary')
|
|
863
|
+
body.expires_at = requireString(args, 'expires_at')
|
|
864
|
+
}
|
|
651
865
|
return call('POST', '/api/tasks', { body, signal: exec?.signal })
|
|
652
866
|
},
|
|
653
867
|
}))
|
|
654
868
|
|
|
869
|
+
writeTools.push(defineMilocoTool({
|
|
870
|
+
name: 'miloco_task_record_init',
|
|
871
|
+
description: '给已存在的长期任务初始化一份活跃记录(POST /api/tasks/{id}/record)。'
|
|
872
|
+
+ 'kind=progress 需 target/unit/window;kind=duration 可给 target_minutes;kind=event 不需要额外字段。',
|
|
873
|
+
parameters: {
|
|
874
|
+
task_id: { type: 'string', description: '任务 id(snake_case,需先创建)' },
|
|
875
|
+
kind: { type: 'string', enum: ['progress', 'duration', 'event'], description: '记录类型' },
|
|
876
|
+
content: {
|
|
877
|
+
type: 'object',
|
|
878
|
+
description: '按 kind 提供的记录参数',
|
|
879
|
+
properties: {
|
|
880
|
+
target: { type: 'integer', minimum: 1, description: 'progress:目标次数' },
|
|
881
|
+
unit: { type: 'string', description: 'progress:计数单位,例如 次 / 杯' },
|
|
882
|
+
window: {
|
|
883
|
+
type: 'string',
|
|
884
|
+
enum: ['day', 'week', 'month', 'longterm'],
|
|
885
|
+
description: 'progress:统计窗口',
|
|
886
|
+
},
|
|
887
|
+
target_minutes: { type: 'integer', minimum: 1, description: 'duration:每日目标分钟数' },
|
|
888
|
+
recurring_pattern: { type: 'object', additionalProperties: true, description: '可选重复规则' },
|
|
889
|
+
expires_at: { type: 'string', description: '可选到期时刻(ISO 8601)' },
|
|
890
|
+
},
|
|
891
|
+
additionalProperties: false,
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
required: ['task_id', 'kind'],
|
|
895
|
+
async run(args, exec) {
|
|
896
|
+
const taskId = requireString(args, 'task_id')
|
|
897
|
+
if (!/^[a-z0-9_]{1,32}$/.test(taskId)) {
|
|
898
|
+
throw new Error('task_id 必须匹配 [a-z0-9_]{1,32}(只能用小写字母、数字、下划线)')
|
|
899
|
+
}
|
|
900
|
+
const kind = requireString(args, 'kind')
|
|
901
|
+
if (!['progress', 'duration', 'event'].includes(kind)) {
|
|
902
|
+
throw new Error('kind 只能是 progress / duration / event')
|
|
903
|
+
}
|
|
904
|
+
const content = args.content && typeof args.content === 'object' ? args.content : {}
|
|
905
|
+
if (kind === 'progress') {
|
|
906
|
+
if (!Number.isInteger(content.target) || content.target <= 0) {
|
|
907
|
+
throw new Error('progress 需要正整数 content.target')
|
|
908
|
+
}
|
|
909
|
+
if (!content.unit) throw new Error('progress 需要 content.unit')
|
|
910
|
+
if (!['day', 'week', 'month', 'longterm'].includes(content.window)) {
|
|
911
|
+
throw new Error('progress 需要 content.window:day / week / month / longterm')
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (kind === 'duration' && content.target_minutes !== undefined) {
|
|
915
|
+
if (!Number.isInteger(content.target_minutes) || content.target_minutes <= 0) {
|
|
916
|
+
throw new Error('content.target_minutes 必须是正整数')
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return call('POST', `/api/tasks/${encodeURIComponent(taskId)}/record`, {
|
|
920
|
+
body: { kind, content },
|
|
921
|
+
signal: exec?.signal,
|
|
922
|
+
})
|
|
923
|
+
},
|
|
924
|
+
}))
|
|
925
|
+
|
|
655
926
|
writeTools.push(defineMilocoTool({
|
|
656
927
|
name: 'miloco_task_delete',
|
|
657
928
|
description: '终止并删除一个长期任务及其关联的规则/定时计划/统计记录。'
|
|
658
929
|
+ 'reason=completed/expired/abandoned;用户主动取消时必须先二次确认。',
|
|
659
930
|
parameters: {
|
|
660
931
|
task_id: { type: 'string', description: '任务 id' },
|
|
661
|
-
reason: { type: 'string', description: 'completed / expired / abandoned' },
|
|
932
|
+
reason: { type: 'string', enum: ['completed', 'expired', 'abandoned'], description: 'completed / expired / abandoned' },
|
|
662
933
|
},
|
|
663
934
|
required: ['task_id', 'reason'],
|
|
664
935
|
async run(args, exec) {
|
|
@@ -679,18 +950,23 @@ export function createMilocoTools(options) {
|
|
|
679
950
|
description: '在后端注册一个定时计划(cron 表达式 / 指定时刻 / 固定间隔),到点后由 Miloco 主动把事件投递给 DSH。',
|
|
680
951
|
parameters: {
|
|
681
952
|
name: { type: 'string', description: '计划名称' },
|
|
682
|
-
kind: { type: 'string', description: 'cron / at / every' },
|
|
953
|
+
kind: { type: 'string', enum: ['cron', 'at', 'every'], description: 'cron / at / every' },
|
|
683
954
|
cron_expr: { type: 'string', description: 'cron 表达式,如 "0 22 * * *"' },
|
|
684
955
|
at_iso: { type: 'string', description: 'ISO 8601 时刻(kind=at)' },
|
|
685
|
-
every_ms: { type: 'integer', description: '
|
|
956
|
+
every_ms: { type: 'integer', minimum: 60000, description: '间隔毫秒,最小 60000(kind=every)' },
|
|
686
957
|
tz: { type: 'string', description: '时区,如 Asia/Shanghai' },
|
|
687
958
|
message: { type: 'string', description: '到点后投递给 Agent 的文本' },
|
|
688
959
|
task_id: { type: 'string', description: '关联的长期任务 id' },
|
|
689
960
|
},
|
|
690
961
|
required: ['kind', 'message'],
|
|
691
962
|
async run(args, exec) {
|
|
963
|
+
const kind = requireString(args, 'kind')
|
|
964
|
+
if (!['cron', 'at', 'every'].includes(kind)) throw new Error('kind 只能是 cron / at / every')
|
|
965
|
+
if (kind === 'cron' && !args.cron_expr) throw new Error('kind=cron 需要 cron_expr')
|
|
966
|
+
if (kind === 'at' && !args.at_iso) throw new Error('kind=at 需要 at_iso')
|
|
967
|
+
if (kind === 'every' && !args.every_ms) throw new Error('kind=every 需要 every_ms')
|
|
692
968
|
const body = {
|
|
693
|
-
kind
|
|
969
|
+
kind,
|
|
694
970
|
message: requireString(args, 'message'),
|
|
695
971
|
name: args.name ?? 'miloco-cron',
|
|
696
972
|
cron_expr: args.cron_expr,
|
|
@@ -703,6 +979,19 @@ export function createMilocoTools(options) {
|
|
|
703
979
|
},
|
|
704
980
|
}))
|
|
705
981
|
|
|
982
|
+
writeTools.push(defineMilocoTool({
|
|
983
|
+
name: 'miloco_cron_delete',
|
|
984
|
+
description: '删除一个定时计划(POST /api/crons 创建的 cron/at/every)。先用 miloco_cron_list 确认 cron_id。',
|
|
985
|
+
parameters: {
|
|
986
|
+
cron_id: { type: 'string', description: '定时计划 id' },
|
|
987
|
+
},
|
|
988
|
+
required: ['cron_id'],
|
|
989
|
+
async run(args, exec) {
|
|
990
|
+
const cronId = requireString(args, 'cron_id')
|
|
991
|
+
return call('DELETE', `/api/crons/${encodeURIComponent(cronId)}`, { signal: exec?.signal })
|
|
992
|
+
},
|
|
993
|
+
}))
|
|
994
|
+
|
|
706
995
|
writeTools.push(defineMilocoTool({
|
|
707
996
|
name: 'miloco_scope_set',
|
|
708
997
|
description: '设置参与感知的摄像头(in_use=true 开始观察,false 停止观察)。这是隐私相关操作,改动前请向用户确认。',
|
|
@@ -744,7 +1033,11 @@ export function createMilocoTools(options) {
|
|
|
744
1033
|
items: {
|
|
745
1034
|
type: 'object',
|
|
746
1035
|
properties: {
|
|
747
|
-
op: {
|
|
1036
|
+
op: {
|
|
1037
|
+
type: 'string',
|
|
1038
|
+
enum: ['add', 'merge', 'replace', 'update', 'delete'],
|
|
1039
|
+
description: 'add / merge / replace / update / delete',
|
|
1040
|
+
},
|
|
748
1041
|
id: { type: 'string', description: '正式区条目 id(merge/replace/update/delete 用)' },
|
|
749
1042
|
from: { type: 'string', description: '候选条目 id(带上即消费该候选)' },
|
|
750
1043
|
entry: {
|
|
@@ -753,13 +1046,17 @@ export function createMilocoTools(options) {
|
|
|
753
1046
|
properties: {
|
|
754
1047
|
type: {
|
|
755
1048
|
type: 'string',
|
|
756
|
-
|
|
1049
|
+
enum: [
|
|
1050
|
+
'member_persona', 'member_health', 'member_routine', 'member_entertain',
|
|
1051
|
+
'member_preference', 'family', 'space', 'device',
|
|
1052
|
+
],
|
|
1053
|
+
description: '成员/家庭/空间/设备档案类型',
|
|
757
1054
|
},
|
|
758
1055
|
subject_id: { type: 'string', description: '成员 person_id' },
|
|
759
1056
|
subject_name: { type: 'string', description: '主体名称(成员称谓 / 空间名)' },
|
|
760
1057
|
content: { type: 'string', description: '条目正文' },
|
|
761
|
-
confidence: { type: 'number', description: '置信度 0~1' },
|
|
762
|
-
source: { type: 'string', description: 'observed / user_told' },
|
|
1058
|
+
confidence: { type: 'number', minimum: 0, maximum: 1, description: '置信度 0~1' },
|
|
1059
|
+
source: { type: 'string', enum: ['observed', 'user_told'], description: 'observed / user_told' },
|
|
763
1060
|
evidence_log: { type: 'array', items: { type: 'string' }, description: '证据' },
|
|
764
1061
|
},
|
|
765
1062
|
required: ['type', 'content'],
|
|
@@ -770,12 +1067,18 @@ export function createMilocoTools(options) {
|
|
|
770
1067
|
additionalProperties: true,
|
|
771
1068
|
},
|
|
772
1069
|
},
|
|
1070
|
+
user_edit: {
|
|
1071
|
+
type: 'boolean',
|
|
1072
|
+
description: '用户直接告知的内容设为 true(后端会按 user_told / 高置信度处理)',
|
|
1073
|
+
},
|
|
773
1074
|
},
|
|
774
1075
|
required: ['ops'],
|
|
775
1076
|
async run(args, exec) {
|
|
776
1077
|
const ops = Array.isArray(args.ops) ? args.ops : []
|
|
777
1078
|
if (ops.length === 0) throw new Error('缺少参数 ops')
|
|
778
|
-
|
|
1079
|
+
const body = { ops }
|
|
1080
|
+
if (args.user_edit !== undefined) body.user_edit = Boolean(args.user_edit)
|
|
1081
|
+
return call('POST', '/api/home-profile/profile:write', { body, signal: exec?.signal })
|
|
779
1082
|
},
|
|
780
1083
|
}))
|
|
781
1084
|
|
|
@@ -789,29 +1092,37 @@ export function createMilocoTools(options) {
|
|
|
789
1092
|
items: {
|
|
790
1093
|
type: 'object',
|
|
791
1094
|
properties: {
|
|
792
|
-
op: {
|
|
1095
|
+
op: {
|
|
1096
|
+
type: 'string',
|
|
1097
|
+
enum: ['add', 'merge', 'update', 'delete'],
|
|
1098
|
+
description: 'add / merge / update / delete',
|
|
1099
|
+
},
|
|
793
1100
|
id: { type: 'string', description: '候选条目 id' },
|
|
794
|
-
date: { type: 'string', description: '
|
|
1101
|
+
date: { type: 'string', description: '观察日期(YYYY-MM-DD)' },
|
|
795
1102
|
entry: {
|
|
796
1103
|
type: 'object',
|
|
797
1104
|
description: '条目内容',
|
|
798
1105
|
properties: {
|
|
799
1106
|
type: {
|
|
800
1107
|
type: 'string',
|
|
801
|
-
|
|
1108
|
+
enum: [
|
|
1109
|
+
'member_persona', 'member_health', 'member_routine', 'member_entertain',
|
|
1110
|
+
'member_preference', 'family', 'space', 'device',
|
|
1111
|
+
],
|
|
1112
|
+
description: '成员/家庭/空间/设备档案类型',
|
|
802
1113
|
},
|
|
803
1114
|
subject_id: { type: 'string', description: '成员 person_id' },
|
|
804
1115
|
subject_name: { type: 'string', description: '主体名称' },
|
|
805
1116
|
content: { type: 'string', description: '条目正文' },
|
|
806
|
-
confidence: { type: 'number', description: '置信度 0~1' },
|
|
807
|
-
source: { type: 'string', description: 'observed / user_told' },
|
|
1117
|
+
confidence: { type: 'number', minimum: 0, maximum: 1, description: '置信度 0~1' },
|
|
1118
|
+
source: { type: 'string', enum: ['observed', 'user_told'], description: 'observed / user_told' },
|
|
808
1119
|
evidence_log: { type: 'array', items: { type: 'string' }, description: '证据' },
|
|
809
1120
|
},
|
|
810
1121
|
required: ['type', 'content'],
|
|
811
1122
|
additionalProperties: true,
|
|
812
1123
|
},
|
|
813
1124
|
},
|
|
814
|
-
required: ['op'],
|
|
1125
|
+
required: ['op', 'date'],
|
|
815
1126
|
additionalProperties: true,
|
|
816
1127
|
},
|
|
817
1128
|
},
|
|
@@ -820,6 +1131,9 @@ export function createMilocoTools(options) {
|
|
|
820
1131
|
async run(args, exec) {
|
|
821
1132
|
const ops = Array.isArray(args.ops) ? args.ops : []
|
|
822
1133
|
if (ops.length === 0) throw new Error('缺少参数 ops')
|
|
1134
|
+
if (ops.some(op => !op || typeof op.date !== 'string' || !op.date.trim())) {
|
|
1135
|
+
throw new Error('candidates:write 的每个 op 都必须带 date(YYYY-MM-DD)')
|
|
1136
|
+
}
|
|
823
1137
|
return call('POST', '/api/home-profile/candidates:write', { body: { ops }, signal: exec?.signal })
|
|
824
1138
|
},
|
|
825
1139
|
}))
|
|
@@ -828,7 +1142,7 @@ export function createMilocoTools(options) {
|
|
|
828
1142
|
name: 'miloco_home_profile_commit',
|
|
829
1143
|
description: '提交家庭档案:后端会自动做过期候选清理(30 天且证据不足 3 条)、token 预算归档与 markdown 渲染持久化。',
|
|
830
1144
|
async run(_args, exec) {
|
|
831
|
-
return call('POST', '/api/home-profile/commit', {
|
|
1145
|
+
return call('POST', '/api/home-profile/commit', { signal: exec?.signal })
|
|
832
1146
|
},
|
|
833
1147
|
}))
|
|
834
1148
|
|
|
@@ -1032,14 +1346,22 @@ export function createMilocoTools(options) {
|
|
|
1032
1346
|
description: '本地图片路径列表',
|
|
1033
1347
|
items: { type: 'string' },
|
|
1034
1348
|
},
|
|
1349
|
+
mode: {
|
|
1350
|
+
type: 'string',
|
|
1351
|
+
enum: ['replace', 'append'],
|
|
1352
|
+
description: 'replace(整组替换,默认)/ append(按质量分保留 top-3)',
|
|
1353
|
+
},
|
|
1035
1354
|
},
|
|
1036
1355
|
required: ['pet_id', 'files'],
|
|
1037
1356
|
async run(args, exec) {
|
|
1038
1357
|
const petId = requireString(args, 'pet_id')
|
|
1039
1358
|
const paths = Array.isArray(args.files) ? args.files.filter(f => typeof f === 'string' && f) : []
|
|
1040
1359
|
if (paths.length === 0) throw new Error('缺少参数 files')
|
|
1360
|
+
const fields = {}
|
|
1361
|
+
if (args.mode !== undefined) fields.mode = args.mode
|
|
1041
1362
|
return client.postMultipart(`/api/identity/pets/${encodeURIComponent(petId)}/reference-crops`, {
|
|
1042
|
-
files: paths.map(p => ({ field: '
|
|
1363
|
+
files: paths.map(p => ({ field: 'crops', path: p })),
|
|
1364
|
+
fields,
|
|
1043
1365
|
signal: exec?.signal,
|
|
1044
1366
|
})
|
|
1045
1367
|
},
|
|
@@ -1141,9 +1463,12 @@ export const WRITE_TOOL_NAMES = [
|
|
|
1141
1463
|
'miloco_scene_trigger',
|
|
1142
1464
|
'miloco_notify_push',
|
|
1143
1465
|
'miloco_rule_create',
|
|
1466
|
+
'miloco_rule_delete',
|
|
1144
1467
|
'miloco_task_create',
|
|
1468
|
+
'miloco_task_record_init',
|
|
1145
1469
|
'miloco_task_delete',
|
|
1146
1470
|
'miloco_cron_create',
|
|
1471
|
+
'miloco_cron_delete',
|
|
1147
1472
|
'miloco_scope_set',
|
|
1148
1473
|
'miloco_home_profile_write',
|
|
1149
1474
|
'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.2",
|
|
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",
|