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/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: '列出米家设备(did、名称、在线状态、所属房间、设备类型)。要控制某台设备前,先用本工具拿到 did。',
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
- return call('GET', '/api/miot/device_list', {
143
- query: args.room ? { room: args.room } : undefined,
144
- signal: exec?.signal,
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: { type: 'string', description: '控制类型:set_property(默认)/ set_properties / call_action' },
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: '创建一条自动化规则。推荐用自然语言描述"当 X 时做 Y",由后端的大模型翻译成条件与动作;'
623
- + '也可以直接给结构化的 condition/actions。',
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
- mode: { type: 'string', description: 'event(默认)/ state' },
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 body = {}
631
- if (args.content) body.content = requireString(args, 'content')
632
- if (args.name) body.name = args.name
633
- if (args.mode) body.mode = args.mode
634
- if (Object.keys(body).length === 0) throw new Error('至少提供 content 或 name')
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: '创建一个长期任务。kind 决定类型:rule=条件触发的自动化,schedule=定时提醒,record=行为累积统计,lifecycle=限时/长期。',
822
+ description: '创建一个长期任务的占位行(POST /api/tasks)。'
823
+ + '任务本身只保存 task_id 与描述;定时提醒再用 miloco_cron_create 关联,'
824
+ + '条件规则用 miloco_rule_create 关联,累计统计用 miloco_task_record_init 初始化。',
642
825
  parameters: {
643
- kind: { type: 'string', description: 'rule / schedule / record / lifecycle' },
644
- content: { type: 'string', description: '任务内容描述' },
645
- name: { type: 'string', description: '任务名称' },
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: ['kind', 'content'],
846
+ required: ['task_id', 'description'],
648
847
  async run(args, exec) {
649
- const body = { kind: requireString(args, 'kind'), content: requireString(args, 'content') }
650
- if (args.name) body.name = args.name
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: '间隔毫秒(kind=every)' },
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: requireString(args, '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: { type: 'string', description: 'add / merge / replace / update / delete' },
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
- description: 'member_persona / member_health / member_routine / member_entertain / member_preference / family / space / device',
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
- return call('POST', '/api/home-profile/profile:write', { body: { ops }, signal: exec?.signal })
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: { type: 'string', description: 'add / merge / update / delete' },
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
- description: 'member_persona / member_health / member_routine / member_entertain / member_preference / family / space / device',
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', { body: {}, signal: exec?.signal })
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: 'file', path: p })),
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.0",
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
- "doc",
33
+ "docs",
34
34
  "scripts",
35
35
  "install.sh",
36
36
  "cordis.patch.yml",