Commit Graph

36 Commits

Author SHA1 Message Date
tigerenwork 55d0f86392 fix: 用 FetchGate actor 修复并发触发丢失问题 2026-07-19 15:10:46 +08:00
tigerenwork b5ad9c1ec3 refactor: 重构设置页面布局,用自定义分区和标签行替代 Form 2026-07-11 01:32:47 +08:00
tigerenwork 7a2508a449 refactor: 移除飞书 API 的 field_names 参数及 configFieldNames 方法 2026-07-11 00:31:50 +08:00
tigerenwork a0b1fb8aa3 chore: 升级版本至 1.4 并将校准设置移至独立 Section 2026-07-10 02:07:43 +08:00
tigerenwork be384c9238 feat: 增强推送可靠性:指数退避重连、校准服务与飞书按人过滤查询 2026-07-10 01:59:05 +08:00
tigerenwork c275c71b55 feat(bitable): 支持通过 SSE 订阅 Bitable 变更并实时刷新
- 新增 BitableEventService,通过 SSE 监听飞书变更推送,收到 change 事件即触发 fetchNow
- AppConfig 新增 feishuAppBaseURL 字段,置空时禁用实时推送、回退轮询
- 设置页新增 "Real-time updates" 区块,支持配置订阅地址并随保存重连
- AppDelegate 启动时自动连接 SSE;断开飞书时同步断开
- 流断开后 30s 自动重连,显式 disconnect 不重连
- Info.plist 放开 ATS 以支持 HTTP 订阅服务(localhost/局域网)
2026-07-08 11:26:47 +08:00
tigerenwork 4d67d8bffd feat: 支持构建时注入飞书配置并优化设置页面体验 2026-07-05 22:57:36 +08:00
tigerenwork 244da37318 fix: 修复设置窗口无法置前的问题 2026-07-05 21:10:51 +08:00
tigerenwork 239403e75c fix: 过滤已关闭/已解决 bug 的变更通知 2026-07-05 20:31:25 +08:00
tigerenwork 55d8b50f1d fix: 增强配置容错与启动稳定性,优化错误状态交互 2026-07-05 20:25:06 +08:00
tigerenwork 7047c51a07 feat: 浮动窗口支持自适应尺寸并锚定右下角 2026-07-05 16:14:58 +08:00
tigerenwork 8286c81afb WIP:feat: capture and log raw response body in Feishu decoding error for better debugging 2026-07-02 10:02:02 +08:00
tigerenwork 3f1367790e fix: timestamp auto-updates without triggering fetch on tray open
- Reverted auto-refresh on tray click (fetch only happens on schedule
  or manual Refresh button)
- Timestamp now uses TimelineView(.periodic) to auto-update every 30s:
  'Just now' → '1m ago' → '5m ago' → etc. without any user action

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 23:54:28 +08:00
tigerenwork 5b1811db10 fix: auto-refresh on tray click, widen 'Just now' to 60s
- Opening the popover now triggers a fetch (via PollerService.fetchNow
  which deduplicates if already in progress)
- 'Just now' now covers 0-60s, then switches to 'Xm ago'.
  Avoids the frozen-timer look since the text only updates on re-render.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 23:51:48 +08:00
tigerenwork 2f2a65f1f7 fix: show 'Just now' instead of '1s ago' after refresh
Replaced RelativeDateTimeFormatter with manual formatting:
<5s → 'Just now', <60s → 'Xs ago', <1h → 'Xm ago',
<1d → 'Xh ago', else 'Xd ago'.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 23:49:23 +08:00
tigerenwork 07ac29faa6 feat: show refresh icon + last-updated time in popover header
Header now always shows: ↻ 2m ago  (or 'Never' before first fetch).
Refresh spinner replaces the text while loading. More discoverable
than the bare relative timestamp that was there before.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 23:45:24 +08:00
tigerenwork 9154264ea7 fix: replace daily time text field with native DatePickers
- Daily schedule mode now shows macOS-native hour:minute DatePickers
  instead of a comma-separated text field
- Each time entry has its own picker with a remove button
- '+ Add time' button to add more entries
- Minimum 1 entry enforced (remove disabled when only 1 left)
- No hardcoded defaults — starts empty, user picks their times

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 21:52:37 +08:00
tigerenwork b81d3edf42 feat: add 8h/12h intervals + daily schedule refresh mode
- Added 8-hour and 12-hour polling interval options
- Added 'Daily schedule' mode (pollIntervalSeconds = -1): fires at
  specific wall-clock times configured in dailyRefreshTimes (e.g. 08:00, 14:00)
- PollerService: in daily mode, checks every 60s and triggers fetch when
  current time matches a scheduled time (with 2-min dedup window)
- Settings UI: shows HH:mm text field when daily schedule is selected
- AppConfig: added dailyRefreshTimes: [String] field

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 21:08:51 +08:00
tigerenwork 64a35752c5 fix: use rawValue for customer field to handle all Feishu field types 2026-06-29 21:03:27 +08:00
tigerenwork fe6dddecf9 fix: move customer between status and age in bug row 2026-06-29 20:57:11 +08:00
tigerenwork 883d7cb608 chore: change customerField default to CustomerName for consistency 2026-06-29 20:51:21 +08:00
tigerenwork 0ce557ac8e feat: add customer field to Bug model with configurable mapping
- Bug model: added optional 'customer' property
- FieldMappings: added 'customerField' (default: 客户名称)
- BugMapper: reads customer value from configured column
- BugRow: displays customer in blue when present
- Settings: Customer field text field in Field Mappings section

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 20:50:46 +08:00
tigerenwork 7590389a04 feat: add Quit button, extended polling intervals
- Popover footer now has a Quit button (power icon) to terminate the app
- Polling interval picker now includes 1h, 2h, 4h, and 1 day options

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 01:17:07 +08:00
tigerenwork 94a3d76af9 fix: badge shows active count, floating widget shows priorities, record URL deep link
1. Menu bar badge and floating widget now show total active bugs count
   (was showing unseen-only count). Unseen dot still works independently
   in the bug list rows.

2. Floating widget now shows priority breakdown:
   🐜 12  🔴3 🟡4 🔵2 3
   Each priority gets a colored dot + count. Only non-zero priorities shown.

3. Fixed Feishu record URL format: now uses query-parameter deep link
   /base/{appToken}?table={tableId}&record={recordId} instead of path
   segments, which opens the specific record row directly in Feishu.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 01:01:01 +08:00
tigerenwork 723c50fc9d feat: configurable status mappings, scrollable settings, case-insensitive assignee
- Added FieldMappings.statusMappings: [FeishuValue: BugStatus] dict
  mapping Feishu select-option text to standard Bugger statuses.
  Pre-populated with common Chinese/English values:
  开发进行中→inProgress, 验收完毕→resolved, Bug Triage→open, etc.
- BugMapper.mapStatus() looks up Feishu value in the mapping,
  falling back to .unknown (shown as active bug).
- Settings: status mapping section with one TextField per status
  showing comma-separated Feishu values. Editable and saved to config.
- Settings: wrapped Form in ScrollView, removed fixed height,
  use minHeight/maxHeight — no more truncated buttons.
- Assignee matching now uses case-insensitive contains instead of
  exact match, handling display name variations from Feishu API.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:51:03 +08:00
tigerenwork 0f8565f65d fix: OAuth token endpoint wraps in {code,message,data} — not flat
The /authen/v1/oidc/access_token response uses the standard Feishu envelope
with 'message' (not 'msg') field. Updated FeishuAPIResponse to handle both
'msg' and 'message' via custom init(from:). exchangeCode and
refreshAccessToken now decode via FeishuAPIResponse<OAuthTokenData>.

Tenant token endpoint (/auth/v3/tenant_access_token/internal) returns flat
response — TenantAccessTokenData decoded directly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:33:53 +08:00
tigerenwork ae918a379c fix: remove redundant CodingKeys conflicting with .convertFromSnakeCase
JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase converts JSON keys
like 'tenant_access_token' to Swift names like 'tenantAccessToken'. Having
explicit CodingKeys that map to the ORIGINAL snake_case keys (e.g.,
case tenantAccessToken = "tenant_access_token") causes a collision:
the decoder first converts the JSON key, then looks for a CodingKey
matching the original name — but the converted name doesn't match.

Removed redundant CodingKeys from OAuthTokenData, TenantAccessTokenData,
UserInfoData, and RecordItem. .convertFromSnakeCase handles all these
mappings automatically.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:30:51 +08:00
tigerenwork f1ef89ef55 fix: tenant_access_token response is flat, not wrapped in data key
The /auth/v3/tenant_access_token/internal endpoint, like the OAuth token
endpoints, returns code, msg, tenant_access_token, and expire at the top
level of the JSON response — NOT nested under a 'data' key.

FeishuAPIResponse<TenantAccessTokenData> decoded successfully but with
data=nil (extra fields silently ignored by JSONDecoder). The guard then
threw FeishuError.apiError(code: 0, message: 'ok') — appearing to succeed
but always failing. This blocked the subsequent OAuth token exchange call.

- Added code/msg fields to TenantAccessTokenData
- tenantAccessToken() now decodes TenantAccessTokenData directly and
  checks data.code != 0 instead of going through FeishuAPIResponse

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:28:39 +08:00
tigerenwork ba56b2385e debug: switch to print() for critical path, manual JSON body
print() goes to stdout which is always visible in Xcode console regardless
of log level filtering. Also replaced JSONEncoder with manual JSON string
construction to rule out any Encodable issues with [String: String].

All critical send() calls now use print() instead of os.Logger.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:26:15 +08:00
tigerenwork f61f8332b8 fix: add step-by-step logs in exchangeCode to pinpoint crash location
The log stops after tenant_token fetch but before the OAuth token exchange
HTTP request. Added per-line logging to find exactly where it dies:
URL construction, body encoding, or the send() call.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:23:39 +08:00
tigerenwork 12e22da9b3 fix: change HTTP request/response logging from .debug to .info level
Debug-level os.Logger messages are filtered out in Console.app by default,
hiding the actual OAuth token exchange request/response. Changed all
FeishuAuthService.send() log calls to .info level so they appear without
changing Console filter settings.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:23:07 +08:00
tigerenwork e275e49795 feat: add comprehensive debug logging to OAuth flow
Added os.Logger-based logging (subsystem: com.xorbitlab.bugger) covering:
- FeishuAuthService.send(): raw HTTP request URL, method, body, auth header,
  response status code, and raw response body (up to 1000 chars)
- exchangeCode(): before/after token exchange with token previews
- TokenManager.handleCallback(): each step with success/failure
- LocalOAuthServer: server start, connection received, code extracted

View logs in: Xcode debug console, or Console.app → filter 'bugger'

Also fixed type inference ambiguity in generic send<T>() calls.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:18:25 +08:00
tigerenwork cf9de54792 fix: decode OAuth token response directly, not via FeishuAPIResponse wrapper
The /authen/v1/oidc/access_token and refresh_access_token endpoints return
token fields (access_token, refresh_token, expires_in) at the top level
of the JSON response, not nested under a 'data' key like standard Feishu
API responses. This caused 'Feishu API error (0): ok' — code 0 with no
wrapped data.

Changed exchangeCode and refreshAccessToken to decode OAuthTokenData
directly from the response. Removed unused decodeAPIResponse helper.

Also removed redirect_uri from token exchange body (not required by
Feishu's token endpoint).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:13:48 +08:00
tigerenwork 74baa22d27 fix: wider settings window, better OAuth error handling, empty refresh token guard
- SettingsWindow: widened to 500pt min, connection result on its own line
  with multiline support (error messages no longer truncated)
- OAuthSetupView: clear statusMessage on error, added 'Try again' button
  when authorization fails, disable Connect button while in progress
- TokenManager: guard against empty refresh token strings (was stored as ''
  when Feishu didn't return refresh_token, causing confusing 'No Feishu
  session' error after access token expired)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 00:09:28 +08:00
tigerenwork 9f8fb52726 feat: switch OAuth redirect to localhost loopback (RFC 8252)
Feishu rejected the 'bugger://' custom URL scheme redirect with 参数不合法.
Switched to the standard desktop OAuth flow: a one-shot local HTTP server
on 127.0.0.1:18923 that receives the authorization code from the browser.

Changes:
- Added LocalOAuthServer (Network.framework, one-shot HTTP listener)
- FeishuAuthService now uses dynamic http://127.0.0.1:PORT/callback redirect
- Removed CFBundleURLTypes from Info.plist (no custom scheme needed)
- Removed NSAppleEventManager handler from AppDelegate
- TokenManager.handleCallback changed from URL to code string
- OAuthSetupView starts server before opening browser
- Added offline_access scope for refresh token support
- generate_xcode_project.py: removed dev cert requirements,
  auto-quotes paths with special chars

Builds successfully with xcodebuild.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-28 23:48:49 +08:00
tigerenwork d9d8ce3e46 feat: full macOS menu bar app implementation
SwiftUI macOS menu bar app for tracking Feishu Bitable bugs:
- Menu bar icon with active bug count badge
- Popover with bug list sorted by priority, click to open in Feishu
- Floating always-on-top widget (NSPanel, optional)
- OAuth 2.0 flow with Feishu Open API (user_access_token + refresh)
- PollerService with configurable interval (default 5 min)
- Diff engine detecting new bugs, status/priority/assignee changes
- Native macOS notifications (UserNotifications framework)
- Keychain-backed token storage
- Settings window with field mapping, polling interval, widget toggle
- Support for custom Bitable column name mappings
- Zero third-party dependencies

17 source files across Models/Services/Views/ViewModels/Utils layers.
Xcode project generated via scripts/generate_xcode_project.py.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-28 20:56:44 +08:00