ReMe/plugins/dingtalk
jinliyl cae613d1c4
refactor(auto-fin): one note per topic, merged by a separate digest step (#558)
* fix(auto-fin): parse topic IDs from fenced JSON replies

* fix(auto-fin): limit report agent tool calls in prompt

* refactor(auto-fin): research news by topic before market open

* fix(config): update default model version for claude_code backend

- Change model version from qwen3.8-max to qwen3.7-plus
- Use environment variable LLM_MODEL_NAME to allow override
- Ensure backend configuration reflects updated model setting

* refactor(auto-fin): write one note per topic before the daily digest

The merge step did two jobs at once: it researched every topic and
combined the results into a single report. Split it the way daily-paper
separates analysis from its brief, so each topic earns a durable note of
its own.

- auto_fin_research_step writes one note per topic that had relevant
  news, tagged `kind: auto-fin-topic` and `topic` in frontmatter
- auto_fin_digest_step merges those notes into the day's brief with no
  tools of its own and appends a `## 主题详解` section linking back to
  each note
- a same-day rerun finds a topic's note by its `topic` frontmatter and
  replaces it in place, deleting the old file when the title changed
- base.py now owns the shared Markdown layer: title sanitizing, report
  normalizing, wikilink validation, note lookup, atomic frontmatter
  writes, and change tracking, so both steps share one write path
- the DingTalk step maps `auto_fin_digest_path` to `markdown_path`
  explicitly instead of relying on whichever step ran last
- drop the unused AutoFinTopicOutput schema and read `job_tools` from
  the step config rather than hardcoding `search`

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(dingtalk): surface rejection details when delivery fails

A failed group send only reported HTTPStatusError, so an operator had to
reproduce the request by hand to learn why DingTalk refused it. Include
the status code and the whitelisted error keys from the response body in
both the log line and the raised RuntimeError.

Only `code`, `message`, and `requestid` are reported: the request body
carries the message content and credentials, so an error response that
echoes it back must not reach the log. Detail is truncated to 200 chars.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): make the suite green on CI

- Point the cookbook claude_code model assertion at qwen3.7-plus, the
  default commit 9404e600 set, so the pre-existing red stops blocking
- Satisfy pylint on the auto-fin tests: prefer implicit booleaness for
  the recorded Agent calls and drop an unused tmp_path fixture

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(auto-fin): name notes after the topic, not the Agent title

The research Agent returned a whole paragraph as its title; that became a
filename and blew past the filesystem's 255-byte name limit, failing with
ENAMETOOLONG inside resolve_note_path. Topics are configured values, so
they are short and predictable - use them for file names and keep the
Agent title in frontmatter.

- Name topic notes after the topic and the digest after the run date
- Fold a byte budget into normalize_title as a safety net for long topics
- Take an AutoFinReportOutput in _write_report instead of loose fields
- Ask both prompts for a short title now that it is display-only

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: isolate per-topic research failures and scope frontmatter reads

A single failing topic used to fail the whole cron job and discard the news
already gathered for the topics that had not run yet -- the 09-19 09:24 run
lost its robot notes that way. Research now logs the failure, continues with
the remaining topics, and only fails the run when no topic produced a note.

frontmatter_read was the only frontmatter step without the _allowed_paths
check that read, write, edit, and frontmatter_update already honour, so an
Agent scoped to one file could still read another file's metadata.

- Isolate per-topic research failures and report them as failed_topics
- Fail loudly when every topic fails so an empty brief is never sent
- Apply _check_path_permission in FrontmatterReadStep

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(auto-fin): keep hand-edited notes and wikilink delimiters from breaking the run

Addresses three review findings on the topic-per-note rework.

- `find_note` and `read_note` now skip a note whose YAML frontmatter does
  not parse. A hand-edited note in the day directory raised
  `yaml.parser.ParserError`, which per-topic isolation surfaced as
  "Auto Fin research failed for every topic" and took the run down with it.
- `normalize_title` also strips `[`, `]` and `#`, which `WikilinkHandler`
  treats as target delimiters. `AI[算力]` used to emit a trailer link the
  parser could not read at all, and `C#` resolved to `.../C` plus an anchor.
- `_write_report` returns the body it actually wrote, and both callers
  propagate it, so the digest answer and the note handed to the digest Agent
  no longer carry links that validation had already downgraded on disk.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-20 14:52:56 +08:00
..
src/reme_dingtalk refactor(auto-fin): one note per topic, merged by a separate digest step (#558) 2026-09-20 14:52:56 +08:00
tests refactor(auto-fin): one note per topic, merged by a separate digest step (#558) 2026-09-20 14:52:56 +08:00
LICENSE feat(plugins): extract DingTalk integration (#551) 2026-09-15 20:05:52 +08:00
pyproject.toml feat(plugins): extract DingTalk integration (#551) 2026-09-15 20:05:52 +08:00
README.md docs(plugins): use local install paths (#552) 2026-09-15 20:15:52 +08:00
README_ZH.md docs(plugins): use local install paths (#552) 2026-09-15 20:15:52 +08:00

DingTalk Plugin

中文

DingTalk provides outbound Markdown notifications and a long-running Agent chat bridge for ReMe. This directory is an independent Python distribution. Its reme.plugins entry point exposes a backend-only plugin.yaml that registers dingtalk_markdown_send_step and dingtalk_wait_step. Applications decide whether and how to use those backends; enabling the plugin alone neither exposes a sending endpoint nor starts a long-lived connection.

Quick start

Install ReMe and the plugin:

python -m pip install "reme-ai[core]>=0.4.1.12"
reme plugins install plugins/dingtalk
reme plugins validate dingtalk

Set the credentials for an internal DingTalk app robot and the target group conversation IDs:

DINGTALK_APP_KEY=your-app-key
DINGTALK_APP_SECRET=your-app-secret
DINGTALK_ROBOT_CODE=your-robot-code
DINGTALK_CONVERSATION_IDS=cid-group-one,cid-group-two

The repository's cookbook config composes DingTalk with the independently installed Auto Fin and Daily Paper plugins:

reme plugins install plugins/auto-fin
reme plugins install plugins/daily_paper
reme start config=cookbook

cookbook.yaml owns the cross-plugin routing: it adds notification Steps after automatic tagging, shares each pipeline with its cron Job, and explicitly starts the DingTalk Agent bridge as a background Job. The business plugins remain usable without DingTalk.

Markdown sending Job example

Add a private one-shot Job to an application config when direct sending is useful:

extends: default
plugins: [dingtalk]

jobs:
  dingtalk_send:
    backend: base
    enable_serve: false
    parameters:
      type: object
      properties:
        markdown_path:
          type: string
      required: [markdown_path]
    steps:
      - backend: dingtalk_markdown_send_step
        app_key: ${DINGTALK_APP_KEY}
        app_secret: ${DINGTALK_APP_SECRET}
        robot_code: ${DINGTALK_ROBOT_CODE}
        conversation_ids: ${DINGTALK_CONVERSATION_IDS:-}
        timeout: 15

Run it without exposing the Job over HTTP or MCP:

reme start config=/path/to/dingtalk.yaml \
  job=dingtalk_send \
  markdown_path=daily/2026-09-15/report.md

markdown_path must resolve to an existing .md file inside the configured workspace. Frontmatter is omitted from the message; the Markdown body is sent serially to each non-empty, comma-separated conversation ID. If no conversation IDs are configured, delivery succeeds as a no-op. Once recipients are configured, missing credentials, invalid paths, empty documents, token failures, and failed recipients make the Job fail explicitly. Delivery metadata reports the configured and successfully sent counts without logging credentials or conversation IDs.

Agent bridge

The bridge maps each sender and conversation to an independent Agent session, supports ReMe's session commands, and sends the final Agent response as Markdown. Add this background Job to an application config that extends the built-in default configuration:

extends: default
plugins: [dingtalk]

jobs:
  dingtalk_agent:
    backend: background
    supervisor: true
    close_timeout: 10
    steps:
      - backend: dingtalk_wait_step
        agent_wrapper: default
        app_key: ${DINGTALK_APP_KEY}
        app_secret: ${DINGTALK_APP_SECRET}
        robot_code: ${DINGTALK_ROBOT_CODE}
        worker_count: 4
        builtin_tools: false
        job_tools: [search, read]

The same background Job is enabled by reme/config/cookbook.yaml. When used independently, start it with reme start config=/path/to/dingtalk.yaml. A background Job is never exposed through HTTP or MCP. Messages from different session keys may run concurrently, while messages sharing a key are serialized. Session IDs live only in ApplicationContext.metadata, so restarting ReMe starts new DingTalk-to-Agent session mappings. The configured agent_wrapper controls model access and permissions; grant only the built-in and Job tools the bot needs.

Development

From the repository root:

reme plugins install ./plugins/dingtalk --editable
reme plugins validate dingtalk
python -m pytest plugins/dingtalk -v

Tests mock DingTalk HTTP, WebSocket, and Agent boundaries and do not contact external services.