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.
@@ -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
- return call('GET', '/api/miot/device_list', {
143
- query: args.room ? { room: args.room } : undefined,
144
- signal: exec?.signal,
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: { type: 'string', description: '控制类型:set_property(默认)/ set_properties / call_action' },
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: '创建一条自动化规则。推荐用自然语言描述"当 X 时做 Y",由后端的大模型翻译成条件与动作;'
623
- + '也可以直接给结构化的 condition/actions。',
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
- mode: { type: 'string', description: 'event(默认)/ state' },
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 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')
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: '创建一个长期任务。kind 决定类型:rule=条件触发的自动化,schedule=定时提醒,record=行为累积统计,lifecycle=限时/长期。',
764
+ description: '创建一个长期任务的占位行(POST /api/tasks)。'
765
+ + '任务本身只保存 task_id 与描述;定时提醒再用 miloco_cron_create 关联,'
766
+ + '条件规则用 miloco_rule_create 关联,累计统计用 miloco_task_record_init 初始化。',
642
767
  parameters: {
643
- kind: { type: 'string', description: 'rule / schedule / record / lifecycle' },
644
- content: { type: 'string', description: '任务内容描述' },
645
- name: { type: 'string', description: '任务名称' },
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: ['kind', 'content'],
788
+ required: ['task_id', 'description'],
648
789
  async run(args, exec) {
649
- const body = { kind: requireString(args, 'kind'), content: requireString(args, 'content') }
650
- if (args.name) body.name = args.name
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: '间隔毫秒(kind=every)' },
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: requireString(args, '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: { type: 'string', description: 'add / merge / replace / update / delete' },
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
- description: 'member_persona / member_health / member_routine / member_entertain / member_preference / family / space / device',
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
- return call('POST', '/api/home-profile/profile:write', { body: { ops }, signal: exec?.signal })
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: { type: 'string', description: 'add / merge / update / delete' },
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
- description: 'member_persona / member_health / member_routine / member_entertain / member_preference / family / space / device',
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', { body: {}, signal: exec?.signal })
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: 'file', path: p })),
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.0",
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
- "doc",
33
+ "docs",
34
34
  "scripts",
35
35
  "install.sh",
36
36
  "cordis.patch.yml",