Workflow (流程) fields like 流转状态 are entirely outside the OpenAPI —
absent from record reads and silently ignored on writes (confirmed by
Feishu engineers in larksuite/openclaw-lark#35). To advance them without
leaving the app, the record page can now be rendered in an embedded
WKWebView window:
- FeishuWebView (persistent cookie store, feishu:// links routed to the
system) in a reusable 1100x800 window via AppDelegate.openWebWindow
- Entry points: "Open in App" toolbar button in the detail view and
"Open in App (Web)" in the popover row context menu
- Web content pins a min size — a bare WKWebView in a hosting controller
collapses the window to ~1px
Also:
- Info.plist now uses $(MARKETING_VERSION)/$(CURRENT_PROJECT_VERSION) so
the project generator is the single version source (it was hardcoded
and overrode build settings)
- Version 1.5 -> 1.6
Phase 1 — read-only detail experience:
- New main window (NavigationSplitView): searchable bug list sidebar with
Active/All filter, detail pane with full metadata, description, and
per-bug Open in Feishu / Copy Link
- Popover row click now opens the in-app detail (Open in Feishu kept via
context menu and footer)
- Display extra table fields: module, source, flow status, fix notes
(new field mappings with Chinese defaults), and screenshot attachments
with in-app thumbnails (media download with bitablePerm extra auth)
- FieldMappings now decodes tolerantly so older persisted configs survive
Phase 2 — editing (status, fix notes, screenshots):
- OAuth scope upgraded from bitable:app:readonly to bitable:app
(requires re-auth; permission errors prompt to reconnect)
- Status picker driven by live field metadata (option names + colors)
- Fix notes text editor with Save/Cancel
- Add screenshots: image picker -> media upload (upload_all) -> append
file tokens to the record
- AttachmentLoader and BugEditService share one field-metadata cache
Note: flow status (流转状态) is a Bitable workflow/Stage field and cannot
be written via the OpenAPI; it stays read-only.
- 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>
- 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>
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>
The Release build wasn't picking up FEISHU_APP_ID / FEISHU_APP_SECRET
because xcodebuild doesn't automatically use Config.xcconfig unless
it's passed with -xcconfig flag. Added auto-detection: if Config.xcconfig
exists, it's used; otherwise a warning is printed.
Co-Authored-By: Claude <noreply@anthropic.com>
Python PIL script generates a bug beetle icon with:
- Dark charcoal body with lighter shell highlight
- Green accent stripe across the wing casing
- Head with white eyes, two curved antennae with dot tips
- Six legs (3 per side)
- All 10 required macOS sizes (16..512 at @1x/@2x)
Run: python3 scripts/generate_app_icon.py
Co-Authored-By: Claude <noreply@anthropic.com>
- 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>
- 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>
- 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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>